Skip to content

Resumability and the Qwik Model

advanced15 min read

The Hydration Tax

Every framework that uses hydration pays the same tax: the browser must download, parse, and execute JavaScript to make the page interactive. Even with partial hydration, selective hydration, or islands — there's always a moment where JS runs before the user can interact.

Qwik asks a radical question: what if hydration didn't exist at all?

Mental Model

Think of a video game. Hydration is like restarting a game from the beginning every time you load it — you replay the intro, rebuild your character, re-acquire your items, until you reach the point where you saved. Resumability is loading a save file. The game drops you exactly where you left off. No replaying, no rebuilding. You're immediately ready to play. Qwik serializes the "game state" (component state, event handlers, execution context) on the server and the browser resumes from that save file.

How Hydration Works (The Problem)

Let's be precise about what hydration costs:

Server renders HTML → sends to browser

Browser:
  1. Download all component JavaScript    (~500KB, takes 800ms on 3G)
  2. Parse and compile the JavaScript     (~300ms on mid-range mobile)
  3. Execute every component function     (~200ms for 500 components)
  4. Rebuild React's fiber tree           (happens during step 3)
  5. Attach event handlers via delegation (~10ms)
  6. Run useEffect callbacks              (~varies)

Total: ~1300ms before page is interactive

Steps 1-4 are pure overhead — the server already did this work. The browser is replaying what the server computed. This replay is the hydration tax, and it scales linearly with page complexity.

How Resumability Works (The Solution)

Qwik eliminates the replay. Instead of the client re-executing components, Qwik serializes the execution state into the HTML itself.

Server:
  1. Execute components
  2. Generate HTML
  3. Serialize component state, closures, and event handler references into HTML attributes
  4. Send enriched HTML to browser

Browser:
  1. Parse HTML (browser does this anyway)
  2. Done. Page is interactive.

No JavaScript download. No component re-execution. No fiber tree rebuild. The page is interactive the moment HTML parses.

<button
  on:click="./chunk-abc.js#handleClick[0]"
  q:id="4"
>
  Add to Cart
</button>

The on:click attribute contains a reference to a lazy-loaded chunk and a specific function within it. When the user clicks, Qwik:

  1. Reads the attribute
  2. Downloads chunk-abc.js (only this tiny chunk)
  3. Calls handleClick with the serialized state
  4. Page responds to the click

The JavaScript for this handler downloads on interaction, not on page load. If the user never clicks this button, its code never downloads.

Quiz
In a resumable framework like Qwik, when does the browser execute component JavaScript?

The Serialization Magic

Qwik's key innovation is serializing things that other frameworks consider impossible to serialize: closures and execution context.

In React, a click handler is a closure:

function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(count + 1)}>{count}</button>
}

The onClick closure captures count and setCount from the component scope. These references are JavaScript runtime constructs — they can't be serialized to HTML and deserialized later. That's why React must re-execute the component to recreate the closure.

Qwik solves this with the $ suffix:

import { component$, useSignal } from '@builder.io/qwik'

export const Counter = component$(() => {
  const count = useSignal(0)

  return (
    <button onClick$={() => count.value++}>
      {count.value}
    </button>
  )
})

The $ marks a serialization boundary. component$ and onClick$ tell Qwik's compiler: "This function should be extractable into its own lazy-loaded chunk. Its captured variables should be serialized."

The compiled output:

<button on:click="./chunk-xyz.js#s_onClick[0]" q:id="5">
  0
</button>
<script type="qwik/json">
  {"refs":{"5":{"count":0}}}
</script>

The component state (count: 0) is serialized as JSON in the HTML. When the click handler downloads, it reads the serialized state and operates on it directly — no component re-execution needed.

Execution Trace
Server Render
Qwik executes component$, resolves state, generates HTML
Standard server rendering — nothing unusual here
State Serialization
Component state, signal values, and closure references serialized into HTML
Embedded as JSON in script tags and DOM attributes
HTML Delivery
Browser receives HTML with embedded state and handler references
No JS bundle to download — just HTML
Instant Interactive
Browser parses HTML. A global listener catches all events.
One tiny Qwik loader script (~1KB) enables all event handling
User Clicks Button
Qwik reads the on:click attribute, downloads the specific handler chunk
Micro-download: just the handler code, not the whole component
Handler Executes
Downloaded handler reads serialized state from the DOM, executes, updates UI
Only this handler's code ran — nothing else downloaded or executed
Quiz
What does the $ suffix in Qwik's component$, onClick$, and useTask$ represent?

Qwik's Global Event Listener

Instead of attaching individual event listeners (React's approach: one delegated listener on root), Qwik uses a global event listener that reads handler references from the DOM.

The Qwik loader is about 1KB of JavaScript — the only JS that runs on page load:

Page loads → Qwik loader (1KB) registers global event listeners
User clicks button → Global listener intercepts the event
Qwik reads the on:click attribute → "./chunk-abc.js#handleClick[0]"
Qwik downloads chunk-abc.js (if not already cached)
Qwik calls handleClick with the serialized context
UI updates

This is a fundamental inversion. Instead of downloading all code and waiting for interactions, you wait for interactions and download only what's needed.

The Tradeoffs

Resumability isn't free. It trades hydration cost for other costs:

FactorHydration (React)Resumability (Qwik)
Page load JSFull bundle (100KB-1MB+)~1KB loader only
Time to InteractiveSeconds (bundle + execution)Near-instant (HTML parse only)
First interaction latencyZero (already hydrated)Small delay (handler downloads on click)
Subsequent interactionsInstantFaster after first (chunks cached)
HTML sizeStandardLarger (serialized state embedded)
Developer experienceFamiliar React patternsNew conventions ($, signals)
EcosystemMassive (React ecosystem)Growing (Qwik-specific)
DebuggingStandard React DevToolsSpecialized tooling needed
Build complexityStandard bundlerCustom compiler + optimizer

The first-interaction latency is the most debated tradeoff. With hydration, once hydration completes, all interactions are instant — the code is already loaded. With resumability, the first click on any component triggers a network request to download its handler. On slow connections, this creates a noticeable delay on first interaction.

Qwik mitigates this with speculative prefetching — using service workers to prefetch handler chunks during idle time, before the user interacts. But it adds complexity.

Common Trap

Resumability shines on initial page load but can feel slower on first interaction if chunks aren't prefetched. On a fast 4G connection, the 50-100ms to download a handler chunk is imperceptible. On a slow 3G connection, it's noticeable. Hydration pays the full cost upfront; resumability spreads the cost across interactions. Neither approach is strictly better — it depends on connection speed, page complexity, and interaction patterns.

Hydration vs Resumability vs Islands: The Full Picture

ApproachInitial JSTTIFirst Click LatencyState SharingEcosystem
Full Hydration (React)Full bundleSlowZero (pre-loaded)Easy (one tree)Massive
Selective Hydration (React 19)Full bundleProgressiveLow (priority-based)Easy (one tree)Massive
Islands (Astro)Island JS onlyPer-islandPer-islandHard (isolated)Multi-framework
RSC (Next.js)Client components onlyModerateZero for hydrated partsEasy (one tree)React ecosystem
Resumability (Qwik)~1KB loaderNear-instantSmall delay (lazy load)Signals-basedGrowing
Quiz
A news website gets 10 million page views per day. Most users read articles (passive) and never click anything interactive. Which rendering approach minimizes wasted JavaScript?
Info

Qwik 2.0 is in beta and brings significant improvements: better serialization, smaller output, and a new import path. The package is moving from @builder.io/qwik to @qwik.dev/core. If you're starting a new Qwik project, check the Qwik 2.0 docs for the latest APIs and migration path.

Why React chose not to implement resumability

The React team has acknowledged resumability's benefits but chosen a different path. Dan Abramov explained that React's architecture assumes components are "pure functions of state" — re-executing them is the core mechanism for updates. Resumability requires a fundamentally different execution model (signals, serializable closures) that's incompatible with React's hooks-based design. Instead, React optimizes through Server Components (eliminate JS for static parts) and selective hydration (prioritize interactive parts). Different philosophy, similar goal.

What developers doWhat they should do
Think resumability means zero JavaScript ever
The page still needs JavaScript to handle interactions. The difference is when it downloads: page load (hydration) vs on demand (resumability).
Resumability means zero JS on page load — handler JS downloads on interaction
Assume resumability is strictly better than hydration
For highly interactive apps where users click many elements, hydration pays its cost once. Resumability pays per interaction (mitigated by prefetching, but still a tradeoff).
Each approach has tradeoffs — resumability trades upfront cost for per-interaction cost
Try to use Qwik patterns in React
Resumability requires compiler-level support for serializable closures. You can't bolt it onto React. Use React's native solutions (RSC, Suspense) instead.
Use React Server Components and selective hydration to minimize JS in the React ecosystem
Dismiss resumability because the ecosystem is smaller
Even if you never use Qwik, understanding resumability helps you appreciate why RSC exists, why Next.js built PPR, and where web rendering is heading.
Understand the concept — it influences how all frameworks evolve
Key Rules
  1. 1Hydration replays server work on the client. Resumability serializes the execution state and skips replay entirely.
  2. 2Qwik's $ suffix marks serialization boundaries — functions that can be lazy-loaded independently.
  3. 3The Qwik loader (~1KB) is the only JS on page load. Handler code downloads on first interaction.
  4. 4Larger HTML (serialized state) is the tradeoff for smaller (or zero) JavaScript on load.
  5. 5Resumability shines for content-heavy, read-mostly sites. Hydration is simpler for highly interactive apps.
  6. 6The concept matters even if you don't use Qwik — it drives innovation in React (RSC), Next.js (PPR), and the entire ecosystem.