Hydration Cost and Alternatives
The Hydration Problem
Hydration is the process where the browser takes server-rendered HTML and makes it interactive by attaching event listeners, re-executing component code, and reconciling the virtual DOM with the real DOM. It's the price you pay for server rendering in every major framework.
Here's the ugly truth: hydration essentially re-does most of the work the server already did. The server rendered your React tree, produced HTML, sent it to the browser. Then the browser downloads 80-200KB of JavaScript, parses it, executes every single component function again, builds a virtual DOM tree, walks the real DOM to match them up, and attaches event listeners. The HTML the server sent? Just a visual placeholder while all this work happens.
Server-side rendering timeline:
Server: |── Fetch data ──|── Render React tree ──|── Serialize to HTML ──|── Send ──|
Browser: |── Parse HTML ──|── Show content ──|── Download JS ──|── HYDRATION ──|── Interactive ──|
^ ^
User sees content User can interact
but nothing works (500-2000ms later)
That gap between "I can see the page" and "I can use the page" is called the uncanny valley of SSR. Users see buttons that don't respond, forms that eat their input, and links that go nowhere. All because hydration hasn't finished.
The Mental Model
Think of hydration like building a house twice.
The server builds the entire house — walls, roof, windows, everything visible. Then it takes a photograph and sends the photo to the client. That photo is the HTML.
The client then rebuilds the entire house from scratch, using the blueprint (JavaScript code). While it's rebuilding, it holds up the photo so you think the house exists. When the rebuild is done, it swaps out the photo for the real house, and now you can open doors and flip switches.
Resumability (Qwik's approach) is like shipping the actual house, not a photo. The house arrives fully built. The light switches already work. No rebuilding needed — the house just... resumes where the server left off.
The Cost Breakdown
What exactly happens during hydration?
Step 1: Download JavaScript
→ 80-200KB of framework + component code
→ Network time: 100-500ms on 4G
Step 2: Parse JavaScript
→ Browser's JS engine parses the code into an AST
→ Parse time: 50-200ms for 100KB on mid-range mobile
Step 3: Execute component functions
→ EVERY component in the tree runs again
→ Hooks execute (useState, useEffect setup, useMemo)
→ Context providers re-establish
→ Execution time: 100-500ms for complex pages
Step 4: Reconciliation
→ React builds virtual DOM tree
→ Walks the server-rendered DOM
→ Matches virtual nodes to real DOM nodes
→ Attaches event listeners
→ Reconciliation time: 50-200ms
Total hydration: 300-1400ms on mid-range mobile
During this entire process, the main thread is blocked. The page looks rendered but is completely unresponsive.
Progressive Hydration
Progressive hydration is a middle ground: instead of hydrating the entire page at once, hydrate components in priority order.
Traditional hydration:
|──────── Hydrate EVERYTHING ────────|
0ms 800ms
Nothing is interactive Everything interactive
Progressive hydration:
|── Hydrate header ──|
0ms 100ms ← Header interactive
|── Hydrate sidebar ──|
150ms 250ms ← Sidebar interactive
|── Hydrate footer ──|
idle ← Footer interactive (when browser is idle)
React 18 introduced selective hydration, which naturally achieves this through Suspense boundaries:
import { Suspense } from 'react'
function Page() {
return (
<div>
<Header /> {/* Hydrates first — it's outside Suspense */}
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar /> {/* Hydrates second */}
</Suspense>
<main>
<Content />
</main>
<Suspense fallback={null}>
<Comments /> {/* Hydrates last — below the fold */}
</Suspense>
</div>
)
}
React 18's selective hydration also handles user interactions during hydration: if a user clicks on a Suspense boundary that hasn't hydrated yet, React prioritizes hydrating that boundary immediately.
User clicks on Comments section (not yet hydrated):
|── Hydrating Content ──| CLICK |── Prioritize Comments ──|── Resume Content ──|
React interrupts current hydration
to hydrate the clicked section first
Resumability: Qwik's Radical Alternative
Qwik doesn't do hydration at all. Instead of re-executing every component on the client, Qwik serializes the application state and event listener locations into the HTML. When the browser loads the page, it doesn't need to execute any JavaScript to make the page interactive — it just reads the serialized information and attaches minimal event handlers.
<!-- What Qwik outputs -->
<button
on:click="./chunk-abc.js#handleClick"
q:id="42"
>
Like (5)
</button>
<!-- The event handler code is NOT downloaded until the button is clicked -->
How resumability works:
Server:
1. Render all components
2. Serialize component state into HTML attributes
3. Serialize event listener references (which file, which function)
4. Send HTML with embedded state
Browser:
1. Parse HTML → page is visible
2. Attach a single global event listener (event delegation)
3. Done — page is interactive
When user clicks the button:
1. Global listener catches the click
2. Reads the handler reference from the DOM attribute
3. Lazy-loads the specific code chunk for that handler
4. Executes the handler
The key difference: traditional hydration downloads and executes all JavaScript upfront. Resumability downloads JavaScript only when the user interacts with a specific component, and only the code for that specific interaction.
The Serialization Format
Qwik serializes three critical pieces of information:
<!-- 1. Component state (serialized as JSON in a script tag) -->
<script type="qwik/json">
{"ctx": {"42": {"count": 5, "liked": false}}}
</script>
<!-- 2. Event handler references (as DOM attributes) -->
<button on:click="./handlers/like.js#onClick[42]">Like (5)</button>
<!-- 3. Component boundaries (as attributes for re-rendering scope) -->
<div q:host q:id="42">
<!-- When state[42] changes, only this subtree re-renders -->
</div>
When the user clicks the button:
- Qwik's global listener catches the event
- It reads
./handlers/like.js#onClick[42]from the attribute - It lazy-loads
handlers/like.js(maybe 2KB) - It calls
onClickwith the deserialized state for component42 - The handler updates state, and Qwik re-renders only the affected subtree
Total JavaScript executed on page load: near zero.
Why resumability is not just lazy loading
You might think: "Can't I just lazy-load my React components to get the same effect?" No, and here's why.
Lazy-loading React components defers downloading the code, but you still need to execute the entire component tree during hydration. The framework runtime, the reconciler, all parent components — they all run before a lazy-loaded child can hydrate.
Resumability skips execution entirely. The framework doesn't need to "discover" the component tree because it's serialized in the HTML. It doesn't need to rebuild state because state is serialized. It doesn't need to find event handlers because handler references are serialized.
Think of it this way: lazy loading in React is like packing your suitcase more efficiently — you still carry everything, just in smaller bags. Resumability is like not packing at all because everything you need is already at your destination.
The Tradeoff Spectrum
Approach | Load JS | Execute on Load | TTI | Interaction Latency
─────────────────────|─────────|─────────────────|────────|─────────────────────
Full hydration | All | All components | Slow | None after hydrated
Progressive hydration| All | Prioritized | Medium | None after hydrated
Partial hydration | Some | Some components | Fast | None for hydrated islands
Resumability | None | None | Instant| Small (lazy load on click)
Resumability has one tradeoff: the first interaction with each component has a small delay while the handler code is lazy-loaded (typically 20-100ms). After that first interaction, subsequent interactions are instant because the code is cached.
Production Scenario: The E-Commerce Landing Page
A team benchmarks the same landing page across different hydration strategies. The page has a hero section, product carousel, customer reviews, FAQ accordion, and a newsletter signup form.
Full hydration (Next.js default):
JS downloaded: 145KB
Hydration time: 1.2s
Time to Interactive: 1.8s
INP (first interaction): 12ms (fast, but only after 1.8s)
Progressive hydration (React 18 with Suspense):
JS downloaded: 145KB
Hydration time: 400ms (hero first, rest deferred)
Hero interactive: 0.6s
Full page interactive: 1.4s
Partial hydration (Astro islands):
JS downloaded: 28KB (carousel + FAQ accordion + newsletter only)
Hydration time: 200ms
Time to Interactive: 0.5s
INP: 15ms
Resumability (Qwik):
JS downloaded on load: 1.2KB (global event listener)
Time to Interactive: 0.1s
First click JS download: 3KB (for the clicked component)
INP (first interaction): 80ms (includes lazy load)
INP (subsequent): 8ms
Common Mistakes
| What developers do | What they should do |
|---|---|
| Assuming hydration is free because the HTML is already on screen The server-rendered HTML is just a visual placeholder. The browser must download, parse, and execute all component JavaScript to make the page interactive. This is often the single largest performance bottleneck. | Hydration re-executes every component on the client, blocking the main thread for hundreds of milliseconds |
| Choosing Qwik/resumability for a highly interactive dashboard If users interact with 40+ components in the first minute, each one triggers a lazy load. The cumulative delay exceeds what full hydration would cost upfront. Resumability optimizes for the common case where users interact with only a few components per page. | Resumability shines for content sites with sparse interactivity. For dashboards where users interact with most components, the lazy-loading overhead on every first interaction adds up |
| Ignoring hydration cost because your laptop benchmarks look fine Developer machines have fast CPUs that mask hydration cost. A page that hydrates in 300ms on your MacBook takes 900ms+ on a mid-range Android phone, which is what most of the world uses. | Test on mid-range mobile devices (Moto G Power, Samsung A series) where hydration can take 2-3x longer |
| Adding React.memo everywhere to speed up hydration React.memo prevents unnecessary re-renders, not initial renders. During hydration, every component must run at least once to build the virtual DOM tree for reconciliation. Memoization only helps on subsequent updates. | React.memo does not help during hydration — components still execute once to build the initial virtual DOM |
Key Rules
- 1Hydration re-executes every component on the client to attach event listeners and reconcile the virtual DOM with server-rendered HTML. It blocks the main thread for 300-1400ms on mobile.
- 2Progressive hydration (React 18 selective hydration) prioritizes hydrating user-interacted Suspense boundaries first, improving perceived interactivity.
- 3Partial hydration (Astro islands) skips hydration for static components entirely. Only interactive islands ship and execute JavaScript.
- 4Resumability (Qwik) eliminates hydration by serializing application state and event handler references into the HTML. Zero JavaScript executes on page load.
- 5Resumability trades a small first-interaction delay (lazy-loading handler code) for near-zero Time to Interactive. Best for content sites with sparse interactivity.
- 6Always benchmark hydration on mid-range mobile devices — developer machines mask the true cost by 2-3x.