Skip to content

Hydration Cost and Alternatives

advanced15 min read

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

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.

Quiz
A page with 200 React components takes 800ms to hydrate on a mid-range mobile device. The user clicks a button at 400ms into hydration. What happens?

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
Quiz
With React 18 selective hydration, a user clicks on a comment reply button that has not hydrated yet. What does React do?

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:

  1. Qwik's global listener catches the event
  2. It reads ./handlers/like.js#onClick[42] from the attribute
  3. It lazy-loads handlers/like.js (maybe 2KB)
  4. It calls onClick with the deserialized state for component 42
  5. 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.

Quiz
A Qwik page has 50 interactive components. On initial load, how much component JavaScript does the browser execute?

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 doWhat 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

Key Rules
  1. 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.
  2. 2Progressive hydration (React 18 selective hydration) prioritizes hydrating user-interacted Suspense boundaries first, improving perceived interactivity.
  3. 3Partial hydration (Astro islands) skips hydration for static components entirely. Only interactive islands ship and execute JavaScript.
  4. 4Resumability (Qwik) eliminates hydration by serializing application state and event handler references into the HTML. Zero JavaScript executes on page load.
  5. 5Resumability trades a small first-interaction delay (lazy-loading handler code) for near-zero Time to Interactive. Best for content sites with sparse interactivity.
  6. 6Always benchmark hydration on mid-range mobile devices — developer machines mask the true cost by 2-3x.