Resumability and the Qwik Model
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?
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:
- Reads the attribute
- Downloads
chunk-abc.js(only this tiny chunk) - Calls
handleClickwith the serialized state - 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.
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.
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:
| Factor | Hydration (React) | Resumability (Qwik) |
|---|---|---|
| Page load JS | Full bundle (100KB-1MB+) | ~1KB loader only |
| Time to Interactive | Seconds (bundle + execution) | Near-instant (HTML parse only) |
| First interaction latency | Zero (already hydrated) | Small delay (handler downloads on click) |
| Subsequent interactions | Instant | Faster after first (chunks cached) |
| HTML size | Standard | Larger (serialized state embedded) |
| Developer experience | Familiar React patterns | New conventions ($, signals) |
| Ecosystem | Massive (React ecosystem) | Growing (Qwik-specific) |
| Debugging | Standard React DevTools | Specialized tooling needed |
| Build complexity | Standard bundler | Custom 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.
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
| Approach | Initial JS | TTI | First Click Latency | State Sharing | Ecosystem |
|---|---|---|---|---|---|
| Full Hydration (React) | Full bundle | Slow | Zero (pre-loaded) | Easy (one tree) | Massive |
| Selective Hydration (React 19) | Full bundle | Progressive | Low (priority-based) | Easy (one tree) | Massive |
| Islands (Astro) | Island JS only | Per-island | Per-island | Hard (isolated) | Multi-framework |
| RSC (Next.js) | Client components only | Moderate | Zero for hydrated parts | Easy (one tree) | React ecosystem |
| Resumability (Qwik) | ~1KB loader | Near-instant | Small delay (lazy load) | Signals-based | Growing |
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 do | What 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 |
- 1Hydration replays server work on the client. Resumability serializes the execution state and skips replay entirely.
- 2Qwik's $ suffix marks serialization boundaries — functions that can be lazy-loaded independently.
- 3The Qwik loader (~1KB) is the only JS on page load. Handler code downloads on first interaction.
- 4Larger HTML (serialized state) is the tradeoff for smaller (or zero) JavaScript on load.
- 5Resumability shines for content-heavy, read-mostly sites. Hydration is simpler for highly interactive apps.
- 6The concept matters even if you don't use Qwik — it drives innovation in React (RSC), Next.js (PPR), and the entire ecosystem.