Hydration

TL;DR / Hydration attaches JavaScript event handlers to server-rendered HTML, turning a static page into an interactive app.

How It Works

 ╭────────────────╮          ╭────────────────────╮
 │     Server     │   HTML   │      Browser       │
 │  Render HTML   │─────────→│ Paint static page  │ <- FCP
 ╰────────────────╯          ╰────────────────────╯
                                        │
                                        │
                                        ↓
                             ╭────────────────────╮
                             │   Uncanny Valley   │
                             │ visible but inert  │
                             ╰────────────────────╯
                                        │
                                        │
                                        ↓
                             ╭────────────────────╮
                             │  Download + parse  │
                             │     JS bundle      │
                             ╰────────────────────╯
                                        │
                                        │
                                        ↓
                             ╭────────────────────╮
                             │  Hydrate: attach   │
                             │   events + state   │ <- TTI
                             ╰────────────────────╯

Edit diagram

When a server-rendered page arrives in the browser, it is just static HTML. Users can see the content, but clicking buttons or submitting forms does nothing because no JavaScript event handlers are attached yet. Hydration is the process that bridges this gap.

During hydration, the framework walks the existing DOM tree and compares it to the virtual DOM that the client-side JavaScript would produce. Rather than throwing away the server HTML and re-rendering from scratch, the framework reconciles the two trees, attaches event listeners, restores component state, and wires up effects. The result is a fully interactive application built on top of the HTML that was already painted to the screen.

The Hydration Timeline

The user experience during hydration follows a specific sequence. First, the browser receives HTML and renders it immediately — this is the First Contentful Paint (FCP). At this point the page looks complete but is inert. Then the JavaScript bundle downloads, parses, and executes. Finally, the framework runs its hydration logic, making the page interactive. The gap between FCP and full interactivity is the hydration cost.

This cost matters because users can see and attempt to interact with the page before hydration completes. They click a button, nothing happens. They type in an input, no validation fires. This creates the "uncanny valley" of server rendering — the page looks ready but is not.

How Frameworks Handle It

React uses hydrateRoot() instead of createRoot() to signal that existing DOM should be reused. During hydration, React builds its fiber tree, attaches event handlers via event delegation on the root, and runs effects. If the server and client trees do not match, React logs a hydration mismatch warning and may fall back to client rendering for the mismatched subtree.

Other frameworks take different approaches. Astro avoids hydration entirely for static content and only hydrates interactive islands. Qwik uses resumability — serializing the application state and event handler locations into the HTML so the browser can resume execution without replaying component logic.

The Performance Trade-Off

Hydration is fundamentally a trade-off. Server rendering gives users fast visual feedback, but full-page hydration means the browser must download and execute all component JavaScript before any interactivity. For large applications, this bundle can be substantial.

The hydration cost scales with the number of components on the page, not just the visible ones. Even components below the fold or hidden behind modals must be hydrated if they are part of the initial render tree. This is why techniques like partial hydration, lazy hydration, and islands architecture exist — they limit the hydration scope to only the components that actually need interactivity.

Hydration vs. Resumability

Traditional hydration replays component logic on the client to reconstruct the framework's internal state. Resumability, pioneered by Qwik, takes a different approach: the server serializes enough information (component boundaries, event handler locations, application state) directly into the HTML so the client can resume where the server left off without re-executing component functions. This eliminates the hydration step entirely. The trade-off is a more complex serialization format and framework-specific constraints, but the result is near-zero startup JavaScript execution regardless of page size.

Gotchas

  • Hydration mismatch errors happen when server HTML differs from what the client would render. Common causes: using Date.now(), Math.random(), or browser-only APIs during render.
  • Full-page hydration blocks all interactivity until the entire JavaScript bundle loads and executes. On slow connections or large apps, this delay can be seconds.
  • Event replay is not automatic. If a user clicks a button before hydration, that click is lost unless the framework explicitly implements event replay (React 18+ does this with Selective Hydration).
  • Third-party scripts can cause mismatches by modifying the DOM between server render and hydration.