How Hydration Works
Dry HTML, Add Water
Here is the best mental model for hydration: the server sends dry HTML — it looks right, has all the content, but it's completely inert. No click handlers, no state, no interactivity. Hydration is the process of adding water (JavaScript) to that dry HTML, bringing it to life.
Imagine you order furniture from IKEA. It arrives as a fully assembled display model (server HTML) — looks perfect, right shape, right color. But it's bolted to a shipping pallet and none of the drawers actually open. Hydration is when the delivery team unbolt it, attach the drawer slides, connect the soft-close hinges, and make everything actually functional. The furniture looked complete before, but now it works.
The technical reality is more nuanced than "just attach event handlers." Let's dig into what React actually does during hydration.
The Hydration Algorithm
When React hydrates, it doesn't throw away the server HTML and re-render from scratch. That would defeat the entire purpose. Instead, React walks the existing DOM and tries to adopt it.
Here's the step-by-step process:
The critical insight: React re-runs your entire component tree on the client during hydration. Every component, every hook, every calculation. It doesn't just attach event handlers — it rebuilds its internal representation of the UI and verifies it matches what the server produced.
import { hydrateRoot } from 'react-dom/client'
hydrateRoot(
document.getElementById('root'),
<App />
)
The hydrateRoot API tells React: "There's already HTML here. Don't create new DOM — adopt what exists and make it interactive."
Event Delegation Under the Hood
React doesn't attach individual event handlers to every element. Instead, it uses event delegation — a single listener on the root DOM node captures all events via bubbling.
function ProductCard({ id }) {
return (
<div onClick={() => track('view', id)}>
<h2 onClick={() => navigate(`/product/${id}`)}>Product Name</h2>
<button onClick={() => addToCart(id)}>Add to Cart</button>
</div>
)
}
React doesn't attach three separate click handlers. It attaches one listener on the root container. When a click event bubbles up, React's event system figures out which component should handle it based on the fiber tree.
During hydration, React sets up this delegation system and maps DOM nodes to their corresponding fibers. This is how a click on the button knows to call addToCart — the fiber tree connects DOM nodes to their handlers.
Why event delegation matters for hydration
Event delegation means React only needs to set up a handful of listeners on the root, regardless of how many interactive elements exist. A page with 1000 buttons doesn't need 1000 listeners — it needs one. This makes the event attachment phase of hydration nearly instant. The expensive part is re-running all the component logic to build the fiber tree.
The True Cost of Hydration
Hydration is expensive, and most developers underestimate why. It's not just "downloading JS." There are three distinct costs:
1. Download cost — The JavaScript for every component on the page must be downloaded. Server Components can't be hydrated (they don't ship JS), but every Client Component in the tree adds to the bundle.
2. Parse and compile cost — The browser's JS engine must parse and compile all that code. On a mid-range Android phone, parsing 1MB of JavaScript takes 2-3 seconds.
3. Execution cost — React re-runs every component function, every hook, every derived computation. If your component tree is 500 components deep, that's 500 function calls plus all their hooks.
Component JS download: 800ms (on 3G)
Parse + compile: 400ms (on mid-range mobile)
Component re-execution: 200ms (500 components)
Event handler setup: 10ms
─────────────────────────────────
Total hydration time: 1,410ms
During this entire window, the page looks loaded but nothing works. Buttons don't respond, forms don't submit, links don't navigate. This is the "uncanny valley" of web performance — the page looks ready, but it's lying to you.
A page can have a great Largest Contentful Paint (LCP) score but terrible Interaction to Next Paint (INP) because of hydration. The server HTML paints fast, but the page isn't actually interactive until hydration completes. Users click buttons that do nothing, creating frustration. Always measure Time to Interactive, not just visual metrics.
Selective Hydration (React 18+)
React 18 introduced selective hydration — instead of hydrating the entire page in one synchronous block, React prioritizes interactive parts based on user behavior. React 19 refined this further with improved scheduling and priority handling.
import { Suspense } from 'react'
function Page() {
return (
<main>
<NavBar />
<Suspense fallback={<HeroSkeleton />}>
<Hero />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
<Footer />
</main>
)
}
Each Suspense boundary creates a hydration boundary. React hydrates them independently, and here's the magic: if the user clicks on an unhydrated section, React prioritizes hydrating that section first.
1. Browser displays server HTML (everything visible)
2. React starts hydrating NavBar (top of page, high priority)
3. User clicks on Comments section
4. React INTERRUPTS NavBar hydration
5. React hydrates Comments immediately (user interaction = highest priority)
6. Comments become interactive — click handler fires
7. React resumes hydrating NavBar, then Hero, then Footer
This is concurrent rendering applied to hydration. React's scheduler detects discrete user events (clicks, key presses) and bumps the hydration priority of the Suspense boundary containing the event target.
Hydration vs Full Client Render
Why not just skip hydration and do a full client render? Because creating DOM elements is expensive.
Full client render (createRoot):
1. Download JS
2. Execute components → build virtual DOM
3. Create ALL DOM elements from scratch
4. Attach event handlers
Time: ~2000ms for large page
Hydration (hydrateRoot):
1. Browser already parsed server HTML into DOM (free!)
2. Download JS
3. Execute components → build virtual DOM
4. Walk existing DOM, adopt nodes
5. Attach event handlers
Time: ~1400ms for same page
Hydration saves the DOM creation step. On a page with thousands of elements, that's significant. But the JS download and execution costs are identical — which is why the biggest hydration optimization is shipping less JavaScript (React Server Components).
| What developers do | What they should do |
|---|---|
| Think hydration only attaches click handlers React needs its full internal representation to manage updates. Event handlers are just the visible tip of what hydration does. | Hydration re-executes ALL component logic, builds the fiber tree, initializes state and effects |
| Assume the page is interactive as soon as content is visible Server HTML is inert — it looks correct but no JavaScript handlers are attached. There's a gap between FCP and TTI. | The page is interactive only after hydration completes (or selective hydration processes that region) |
| Try to optimize hydration by reducing DOM elements DOM walking is cheap. The expensive parts are JS download, parsing, and component re-execution. Ship less JS. | Reduce JavaScript bundle size and use Server Components to skip hydration entirely |
| Wrap every component in Suspense for selective hydration Too many boundaries add overhead. Place them at natural content sections that can load independently. | Use Suspense boundaries at meaningful UI sections (nav, content, sidebar, comments) |
- 1Hydration adopts existing server HTML — it does not recreate the DOM.
- 2React re-executes ALL client components during hydration to build its fiber tree.
- 3The hydration cost is JS download + parse + execution, not DOM creation.
- 4Selective hydration (React 18+) prioritizes user-interacted Suspense boundaries.
- 5The gap between visible content and working interactivity is the 'uncanny valley' — measure TTI, not just LCP.
- 6The best hydration optimization is shipping less JS — use React Server Components.