Closing the Hydration Gap: Optimizing First Contentful Paint in Reach
Learn how to eliminate the 'blank screen' effect in Reach by implementing Server-Side Rendering (SSR) and hydration to optimize First Contentful Paint.
27 Oct 2025, 07:49 UTC

The Problem: The Blank Screen Effect
When building with client-side rendering (CSR), the browser receives a nearly empty HTML file and a large JavaScript bundle. The user sees a blank screen until the browser downloads, parses, and executes that script to build the DOM. This delay directly inflates your First Contentful Paint (FCP)—the time it takes for the first piece of content to appear on the screen.
The solution is a hybrid approach using Server-Side Rendering (SSR) and hydration. By pre-rendering the component tree on the server, you deliver a fully formed HTML document immediately. However, this introduces a new challenge: the Hydration Gap. This is the period where the page looks complete but is non-interactive because the JavaScript event listeners haven't yet attached to the static HTML.
Pre-rendering the Component Tree
To eliminate the initial blank screen, the server must transform the Reach component tree into a static string. This happens in a Node.js environment before the response is sent to the client. Instead of sending an empty root div, the server executes the render logic and injects the resulting HTML directly into the body.
This process shifts the rendering workload from the user's device to your server. While this improves the perceived load speed, it means the server is now performing CPU-intensive string concatenation and component traversal for every request.
The Hydration Process
Hydration is the process of "watering" the dry, static HTML sent by the server. Rather than wiping the DOM and re-rendering everything from scratch—which would cause a visible flicker—the client-side Reach runtime scans the existing DOM nodes and maps them to the internal component state.
During hydration, the framework attaches event listeners (like onClick or onChange) to the existing elements. If the server-rendered HTML matches the client's initial render exactly, the transition is seamless. If they differ, a hydration mismatch occurs, often forcing the browser to discard the server HTML and perform a full client-side render, defeating the purpose of SSR.
Implementation Example
Below is a conceptual implementation of an SSR flow. This assumes a Node.js server environment and a Reach-compatible client entry point. Verify the exact API names (renderToString, hydrateRoot) against your framework version's documentation before deploying.
Server-Side Logic (Node.js)
// Run on the server; requires permission to bind an HTTP port
import express from 'express';
import { renderToString } from 'reach-server';
import App from './src/App';
const app = express();
app.get('*', (req, res) => {
// Convert the component tree to a static HTML string
const appHtml = renderToString(<App />);
res.send(`
<!DOCTYPE html>
<html>
<head><title>Reach SSR App</title></head>
<body>
<div id="root">${appHtml}</div>
<script src="/bundle.js"></script>
</body>
</html>
`);
});
app.listen(3000);
Client-Side Logic (Entry Point)
// Runs in the browser via bundle.js
import { hydrateRoot } from 'reach-dom/client';
import App from './App';
// hydrateRoot attaches listeners to the existing #root DOM
hydrateRoot(document.getElementById('root'), <App />);
Trade-offs and Limitations
Implementing SSR is not a free win; it involves a fundamental trade-off between Time to First Byte (TTFB) and First Contentful Paint (FCP).
| Metric | Client-Side Rendering | Server-Side Rendering |
|---|---|---|
| TTFB | Fast (Static HTML) | Slower (Server must render) |
| FCP | Slow (Wait for JS) | Fast (Immediate HTML) |
| Server Load | Low (CDN delivery) | High (CPU per request) |
Critical Warning: Avoid using non-deterministic data during the render phase. If you use Math.random() or new Date() inside your component, the server will generate one value and the client another. This will trigger a hydration error in the browser console, potentially degrading performance.
Verifying the Result
To confirm your SSR implementation is working correctly, follow these steps:
- Inspect Source: Right-click the page and select "View Page Source". If you see your actual content inside the
#rootdiv rather than an empty tag, SSR is functioning. - Network Analysis: Open Browser DevTools > Network tab. Check the first document request; the response body should contain the pre-rendered HTML.
- Console Check: Refresh the page. If you see warnings regarding hydration failing because the initial UI does not match, you have a mismatch in your server and client render logic.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.