August 24th, 2021
  • 5 mins
  • 770 words, 5k chars
static site generator, react, server-side rendering, gatsby

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.

Build pipeline overview presentation

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.

Figure of GET request

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.

Figure of another GET request

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 windowless 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.

Thank you!

You should get a confirmation email soon. I'll keep the content coming up!

- Kimmo

Like the content?

Let me know by subscribing to new posts.