DIY Gatsby: Static site rendering
This is Part I of a series where we'll look into how kimmo.blog works. Part I covers SSR and static site basics. If you are familiar with the topic, feel free to jump into Part II: Module bundling.
Motivation has been covered in Writing, or coding.
Disclaimers
Gatsby is a production-ready tool that has everything you need: customisable data sources accessed via GraphQL, optimised build pipeline, functionality via plugins, and a huge community.
The build system that we'll go through on the other hand is experimental and doesn't even have frontend routing. Building a better Gatsby is not the point. Instead, we want to understand how it works and possibly make the site's bundle footprint even smaller.
That out of the way, let's jump into building the thing.
Static site rendering
The high-level idea is pretty straightforward: convert a bunch of source files and content to a static site in output/
directory.
We "simply" need to implement the Build pipeline box in the middle.
One way to start figuring out a system is from the user's perspective. Let's imagine there's a file server that serves everything under output/
directory.
When a user loads /posts/
path in their browser, the server responds with the contents of output/posts/index.html
file. The contents have been created with Node.js using React:
import ReactDOMServer from "react-dom/server";import { AllPostsPage } from "./pages/AllPosts";
ReactDOMServer.renderToString(<AllPostsPage />);
This type of React usage is often called server-side rendering – regardless of where the rendering technically took place. renderToString()
might've been called on your local machine, Netlify's build servers, or per request on a Node.js server.
Why do we even need this step? React apps work just fine even if the backend sends an empty
<div id="react-root"></div>
container.
That's true. Server-side rendering is not mandatory, but it has some benefits. It'll make the content load faster, more SEO friendly, and accessible even when JS has been disabled.
That covers the read-only content, but for richer interaction, we need to generate a JS bundle that kicks off the dynamism browser-side. The traditional way would be to render HTML directly from e.g. Markdown, and JS would refer to the existing DOM elements via query selectors:
const elements = document.querySelectorAll('.chart');initCharts(elements);
There are many benefits to the traditional model, no doubt about it. However, it's missing some of the ergonomics we get from React — for example, component separation and state handling. Anyways, arguing about whether using React in this context makes sense is not the topic of this post.
Building on top of the previous example, let's add a JS bundle called hydrate.js
to the page. It uses ReactDOM.hydrate() to continue where the server-side rendered React left off:
import React from "react";import ReactDOM from "react-dom";import { AllPostsPage } from "./pages/AllPosts";
window.addEventListener("load", () => { ReactDOM.hydrate( <AllPostsPage />, document.getElementById('react-root') );});
The DOM stays untouched (<h1>All posts</h1>
, etc), but React starts event listeners to allow user interaction on the page.
That's how server-side rendering works. Backend returns a frozen snapshot of a React component and frontend brings it back to life.
There are benefits, but definitely some downsides too. First of all, the rendering flow is very complex compared to a single page app. Another annoyance is that most component code paths will be run on Node.js too. Many frontend libraries throw ReferenceError: window is not defined
because they weren't designed to be run in a window
less environment. That means you'll have one more scenario to test.
SPA vs static site
In single page apps, the server is usually configured to send the same index.html
with an empty <div id="react-root" />
container for any request path. JavaScript would then read window.location
and decide which page content to render based on the frontend routing. But we don't want that, because it will inevitably be at least a little bit slower than the server-side rendered content at page load.
In static sites, each page needs to be able to independently bootstrap the JavaScript UI. In other words, static sites can have multiple entrypoints into the application.
I wouldn't say there's a clear winner, just different approaches with varying pros and cons. Also, I'm going to conveniently just skip the fact that the custom build system doesn't support frontend routing. That will make navigation after the initial page load slightly slower than in SPAs. It was a shortcut that I took to make the setup less complex. Hats off to Gatsby for actually managing the complexity.
That's static site rendering basics. There's a ton of details that still need to be solved though. In Part II, we'll look into code specifics and module bundling with Rollup.