Skip to main content

What is hydration?

Hydration is the process of attaching React’s event listeners and state management to server-rendered HTML, transforming static markup into a fully interactive application.
Think of hydration as “bringing server-rendered HTML to life” by connecting React’s virtual DOM to the actual DOM elements already rendered on the page.

The hydration process

1

Server renders HTML

The server executes React components and generates HTML markup. This HTML is sent to the client as part of the initial page load.
2

Browser displays HTML

The browser receives and displays the server-rendered HTML immediately. Users can see the content, but it’s not yet interactive.
3

JavaScript loads

The browser downloads and executes the React JavaScript bundle that contains the same components used for server rendering.
4

React hydrates

React renders the component tree in memory and matches it against the existing DOM. It then attaches event handlers and makes the page interactive.
import { renderToString } from 'react-dom/server';
import App from './App';

const html = renderToString(<App />);
// Returns: <div><button>Click me</button></div>

The double pass problem

The double pass problem occurs when React must render components twice: once on the server and once on the client. This creates several challenges that impact performance and user experience.

Core issues

The same component tree is rendered twice—first on the server to generate HTML, then on the client to build the virtual DOM for hydration. This wastes CPU cycles and increases the total time to interactivity.
// This component renders twice
function ExpensiveComponent() {
  // Expensive calculations happen on both server and client
  const data = processLargeDataset();
  return <div>{data}</div>;
}
If the server and client render different output, React throws hydration mismatch warnings. This can happen due to:
  • Browser-only APIs (window, document)
  • Random values or timestamps
  • User-specific content
  • Environment differences
// Causes hydration mismatch
function BrokenComponent() {
  return <div>{Math.random()}</div>; // Different on server vs client
}
Hydration mismatches can cause React to discard server-rendered HTML and re-render from scratch, negating SSR performance benefits.
Traditional hydration blocks the entire page from being interactive until all components have hydrated. A single slow component can delay interactivity for the entire application.
Server-side data must be serialized and embedded in HTML, then deserialized on the client. Large data sets increase HTML size and parsing time.
// Data embedded in HTML
<script>
  window.__INITIAL_DATA__ = {/* potentially huge object */};
</script>

Solutions to the double pass problem

Modern React provides several strategies to mitigate the double pass problem and improve hydration performance.

Streaming server-side rendering

Send HTML to the client incrementally as components finish rendering, rather than waiting for the entire tree.
import { renderToPipeableStream } from 'react-dom/server';

const { pipe } = renderToPipeableStream(<App />, {
  onShellReady() {
    // Send initial shell immediately
    pipe(res);
  }
});
Benefits:
  • Faster Time to First Byte (TTFB)
  • Progressive content delivery
  • Better perceived performance

Edge rendering vs Node rendering

The choice of rendering environment significantly impacts performance, scalability, and capabilities.

Edge rendering

Execute server-side rendering on edge locations close to users, using lightweight JavaScript runtimes like Cloudflare Workers or Vercel Edge Functions.Architecture:
  • Distributed globally across CDN edge nodes
  • Runs in V8 isolates, not full Node.js
  • Minimal cold start times (< 10ms)
  • Limited runtime capabilities
Advantages:

Lower latency

Rendering happens closer to users, reducing round-trip time for initial HTML.

Faster cold starts

V8 isolates start nearly instantly compared to Node.js processes.

Better scalability

Edge functions auto-scale across global infrastructure without manual configuration.

Cost efficiency

Pay-per-request pricing and no idle server costs.
Limitations:
// Not available in Edge runtime
import fs from 'fs'; // ❌ No file system access
import { exec } from 'child_process'; // ❌ No process spawning

// Limited APIs
const buffer = new ArrayBuffer(1024 * 1024 * 100); // ❌ Memory limits
Edge runtimes have strict limitations: no file system access, no native modules, limited memory, and shorter execution timeouts.
Best for:
  • Static or cache-friendly content
  • Global applications requiring low latency
  • Simple rendering without complex Node.js dependencies
  • Applications prioritizing cold start performance

Choosing the right environment

1

Assess your dependencies

If your application requires Node.js-specific APIs, native modules, or file system access, Node rendering is necessary. If you only use web-standard APIs, Edge rendering is viable.
2

Consider your users

For globally distributed users, Edge rendering provides better latency. For regional applications or those with centralized data, Node rendering may be sufficient.
3

Evaluate complexity

Simple rendering logic benefits from Edge’s instant cold starts. Complex server-side processing may require Node’s full capabilities.
4

Test and measure

Profile both options with your actual application. Measure TTFB, hydration time, and total cost to determine the best fit.
Many modern frameworks like Next.js support hybrid approaches, allowing you to use Edge rendering for some routes and Node rendering for others based on specific requirements.

Build docs developers (and LLMs) love