Faster first paint without a server

Large single-page applications often leave users staring at a blank screen while JavaScript bundles download and execute. Server-side rendering fixes that but introduces its own complexity and a slower Time to First Byte. react-snap offers a middle path: pre-rendering static HTML files for every route at build time, which can improve First Paint without requiring you to run a rendering server.

Houssein Djirdeh

The technique uses a headless browser to crawl your app's routes and generate the corresponding HTML payloads. Those files ship alongside your normal JavaScript bundles. When a user hits a route, the pre-rendered HTML paints immediately, and the existing JS bootstraps the interactive app on top of it.

A side by side loading comparison. The version using pre-rendering loads 4.2 seconds faster.

Setting up react-snap

react-snap is built on Puppeteer. Install it as a development dependency:

npm install --save-dev react-snap

Next, add a postbuild script to your package.json so the tool runs automatically after every build:

"scripts": {
  //...
  "postbuild": "react-snap"
}

The final setup step is adjusting how your app boots. Update src/index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));
const rootElement = document.getElementById("root");

if (rootElement.hasChildNodes()) {
  ReactDOM.hydrate(<App />, rootElement);
} else {
  ReactDOM.render(<App />, rootElement);
}

The key change is the check for existing child nodes. When pre-rendered or server-rendered HTML is already in the DOM, the app calls ReactDOM.hydrate instead of ReactDOM.render. This attaches event listeners to the existing markup rather than creating the DOM from scratch.

After building, static HTML files are generated for every crawled route. In Chrome DevTools, you can click the URL of the HTML request and switch to the Previews tab to inspect the payload.

A before and after comparison. The after shot shows content has rendered.

Dealing with unstyled HTML

Pre-rendered HTML will appear immediately but unstyled, potentially causing a flash of unstyled content (FOUC). This is more noticeable with CSS-in-JS libraries, where the JS bundle must finish running before any styles apply.

The fix is to inline critical CSS into the <head> of each pre-rendered HTML document. react-snap delegates this to minimalcss, which extracts the minimal CSS required for the initial page render. Enable it with the inlineCss option in package.json:

"reactSnap": {
  "inlineCss": true
}

With this enabled, the response preview in DevTools shows the fully styled page, with the critical CSS embedded directly in the HTML.

A before and after comparison. The after shot shows content has rendered and is styled because of inlined critical CSS.

Practical recommendations

If you're not server-side rendering, react-snap is a low-effort way to deliver static HTML to users:

  1. Install it as a development dependency and start with the default settings.
  2. Try the experimental inlineCss option to eliminate FOUC if it works for your site.
  3. Be careful with component-level code splitting inside routes: pre-rendering can otherwise capture a loading state. The react-snap README has details on handling async components.