Qwik: Resumability vs Hydration Explained
Discover how Qwik's resumability eliminates hydration overhead and delivers instant-loading web apps. See side-by-side code comparisons with React and learn when to choose resumability over traditional hydration.
While I was looking over some server-side rendering strategies the other day, I came across Qwik and its concept of "resumability." I was once guilty of thinking all SSR frameworks worked the same way—render HTML on the server, ship JavaScript to the client, and let hydration do its magic. Little did I know that hydration itself was the bottleneck I'd been fighting for years.
The Hydration Problem in Modern Web Frameworks
Let me paint a picture. You've built a beautiful React app with server-side rendering. Your users get that sweet first contentful paint almost instantly. But then they click a button and... nothing happens for a second or two. Under the hood, your framework is frantically re-executing all your components to "hydrate" them—attaching event listeners, rebuilding the component tree, and restoring application state.
I cannot stress this enough! Even though your user sees the content immediately, they can't interact with it until hydration completes. On mobile devices or slower networks, this creates a frustrating dead zone where your app looks ready but isn't.
The worst part? The server already did all that work. It rendered the components, figured out what should be on screen, and sent the HTML. But the client throws all that knowledge away and starts from scratch.

Understanding Hydration: How Traditional Frameworks Work
When I finally decided to dig deeper into how frameworks like React, Vue, and Svelte handle SSR, I realized they all follow the same pattern. The server runs your code, generates HTML, and serializes some state. Then the client:
- Downloads all the JavaScript bundles
- Re-executes every component
- Rebuilds the entire virtual DOM or component tree
- Attaches event listeners to the rendered HTML
- Reconciles the server HTML with what it just computed
This process is called hydration because you're "rehydrating" the static HTML with interactivity. In other words, you're repeating on the client what the server already accomplished.
Here's what a typical hydrated React component looks like:
// Counter.tsx - Traditional React with hydration
import { useState } from 'react'
export default function Counter() {
// This runs on both server AND client
const [count, setCount] = useState(0)
console.log('Counter component executing')
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
)
}When this component hydrates, you'll see "Counter component executing" log twice—once on the server, once on the client. The entire component function runs again just to attach that onClick handler.
Resumability Explained: Qwik's Revolutionary Approach
Qwik takes a fundamentally different approach. Instead of hydration, it uses resumability. Fascinating! The idea is simple but powerful: pick up exactly where the server left off, without re-executing any code.
Luckily we can achieve this through clever serialization. Qwik serializes not just your state, but also the location of event handlers and component boundaries. The HTML that reaches your browser contains everything needed to make the app interactive without downloading or executing component code upfront.
Think of it like a video game save file. Traditional frameworks force you to replay the entire game from the beginning (hydration). Qwik lets you load your save and continue exactly where you were (resumability).
The client only downloads and executes the code for the specific interactions the user triggers. Click a button? Download and run just that button's handler. Hover over a menu? Load just the menu logic.
Code Execution: Server vs Client in Hydration vs Resumability
Let me show you the same counter component in Qwik to illustrate the difference:
// counter.tsx - Qwik with resumability
import { component$, useSignal } from '@builder.io/qwik'
export default component$(() => {
const count = useSignal(0)
console.log('Counter component executing')
return (
<div>
<p>Count: {count.value}</p>
<button onClick$={() => count.value++}>
Increment
</button>
</div>
)
})Notice the $ suffix on component$ and onClick$? That's Qwik's optimizer signal. It tells the framework "this can be lazy-loaded."
Here's where it gets wonderful! When this component renders on the server, you see "Counter component executing" once. But on the client? Nothing logs until you actually click the button. The component function doesn't re-execute during "hydration" because there is no hydration.

The server-rendered HTML includes serialized information about where the click handler lives. When you click, Qwik downloads just that tiny handler function and executes it. The rest of the component code never loads unless needed.
Side-by-Side Comparison: React Hydration vs Qwik Resumability
Let's look at what actually happens with a more realistic example—a product listing with a "Add to Cart" button:
// React version - All code runs on client during hydration
function ProductCard({ product }) {
const [isAdding, setIsAdding] = useState(false)
// This complex calculation runs during hydration
const discountedPrice = calculateDiscounts(product)
const recommendations = getRecommendations(product)
const addToCart = async () => {
setIsAdding(true)
await api.addToCart(product.id)
setIsAdding(false)
}
// All of this rebuilds on client
return (
<div className="product">
<h3>{product.name}</h3>
<p>{discountedPrice}</p>
<RecommendationList items={recommendations} />
<button onClick={addToCart} disabled={isAdding}>
Add to Cart
</button>
</div>
)
}In React, calculateDiscounts and getRecommendations run on the server, then run again on the client during hydration. The RecommendationList component also re-executes. All this work happens before the user can click anything.
Now the Qwik version:
// Qwik version - Only loads what's needed, when it's needed
export const ProductCard = component$(({ product }) => {
const isAdding = useSignal(false)
// These only run on server
const discountedPrice = calculateDiscounts(product)
const recommendations = getRecommendations(product)
return (
<div class="product">
<h3>{product.name}</h3>
<p>{discountedPrice}</p>
<RecommendationList items={recommendations} />
<button
onClick$={async () => {
// This code only downloads/runs when clicked
isAdding.value = true
await api.addToCart(product.id)
isAdding.value = false
}}
disabled={isAdding.value}
>
Add to Cart
</button>
</div>
)
})When this page loads in the browser, zero component code executes. The calculations, the recommendations logic—none of it re-runs. The button is immediately interactive because Qwik serialized the handler location into the HTML. Click it, and only then does the handler code download and execute.
The Three Problems Qwik Solves: Listeners, Component Tree, and State
I realized that resumability solves three specific hydration problems:
Event listeners: Traditional frameworks must traverse the DOM and reattach all event listeners. Qwik serializes listener locations in HTML as on:click="./chunk-abc123.js#handler" attributes. No traversal needed.
Component tree: Frameworks rebuild the entire component hierarchy to know what rendered where. Qwik serializes component boundaries with special HTML comments and attributes. It knows the tree structure without re-executing components.
Application state: Frameworks serialize state and then rehydrate it by running all component code again. Qwik serializes state directly into the HTML and references it in place. State is already where it needs to be.
Building a Real-World Interactive Component with Qwik
Let me show you a practical example I built recently—a search input with debounced API calls:
// search-bar.tsx
import { component$, useSignal, useTask$ } from '@builder.io/qwik'
export const SearchBar = component$(() => {
const query = useSignal('')
const results = useSignal([])
const isSearching = useSignal(false)
useTask$(async ({ track, cleanup }) => {
// Track changes to query
const searchTerm = track(() => query.value)
// Debounce logic
const timeoutId = setTimeout(async () => {
if (searchTerm.length > 2) {
isSearching.value = true
const data = await fetch(`/api/search?q=${searchTerm}`)
.then(r => r.json())
results.value = data
isSearching.value = false
}
}, 300)
cleanup(() => clearTimeout(timeoutId))
})
return (
<div class="search">
<input
type="text"
value={query.value}
onInput$={(e) => query.value = e.target.value}
placeholder="Search products..."
/>
{isSearching.value && <div>Searching...</div>}
<ul>
{results.value.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
)
})The beautiful thing here? The useTask$ effect and debounce logic only download and execute when the user starts typing. The server renders the empty search bar. When you type, Qwik lazily loads just the input handler. When that updates query, it loads the useTask$ code. Everything loads progressively, on-demand.
In a hydration-based framework, all this code would execute immediately on page load, even if the user never touches the search bar.
Performance Implications and When to Choose Resumability
After working with Qwik on several projects, I've found the performance gains are most dramatic when:
- You have content-heavy pages with interactive elements
- Your users are on mobile devices or slower connections
- Time-to-interactive matters more than total load time
- You have complex components that don't need immediate interactivity
The trade-off? More HTTP requests for lazy-loaded chunks. But with HTTP/2 multiplexing and edge caching, these small requests are negligible compared to hydration costs.
I was once guilty of optimizing bundle sizes while ignoring execution time. Qwik taught me that the fastest JavaScript is the JavaScript you never download or execute.
Resumability isn't always necessary. For highly interactive apps like video editors or real-time dashboards, you need most code upfront anyway. But for content sites, e-commerce, blogs, and marketing pages? Resumability offers instant interactivity that hydration simply cannot match.
And that concludes the end of this post! I hope you found this valuable and look out for more in the future!