Resumability is Qwik's breakthrough innovation that eliminates the need for hydration, resulting in instant page loads and minimal JavaScript execution. This comprehensive guide explores how resumability works and why it matters.
What is Resumability?
Resumability is the ability of an application to continue execution on the client exactly where it left off on the server. Instead of replaying the entire rendering process (hydration), Qwik resumes the application state and functionality directly.
Traditional Hydration vs Resumability
Traditional Framework Hydration:
- 1Server renders HTML
- 2Client downloads entire framework
- 3Framework executes to recreate component tree
- 4Event listeners are attached
- 5Application becomes interactive
Qwik Resumability:
- 1Server renders HTML
- 2Client downloads minimal JavaScript
- 3Application resumes immediately
- 4Event listeners loaded on-demand
- 5Instant interactivity
How Resumability Works
Serialization of State
Qwik serializes the application state during server-side rendering:
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const count = useSignal(0);
return (
<div>
<span>Count: {count.value}</span>
<button onClick$={() => count.value++}>
Increment
</button>
</div>
);
});During SSR, Qwik:
- 1Renders the HTML
- 2Serializes component state
- 3Closures are captured and serialized
- 4Event listeners are registered in the HTML
Lazy Loading Everything
Qwik breaks everything into lazy-loadable chunks:
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const showDetails = useSignal(false);
return (
<div>
<button onClick$={() => showDetails.value = true}>
Show Details
</button>
{showDetails.value && (
<div>
{/* This content is lazy loaded */}
<p>These details are only loaded when needed!</p>
</div>
)}
</div>
);
});The Optimizer
Qwik's Optimizer transforms your code into resumable chunks:
Before Optimization
export const Counter = component$(() => {
const count = useSignal(0);
return <button onClick$={() => count.value++}>{count.value}</button>;
});After Optimization
The Optimizer creates:
- 1A component chunk (loaded when component renders)
- 2An event handler chunk (loaded when button clicked)
- 3State serialization (included in HTML)
Event Listener Resumability
Event Listener Registration
import { component$ } from '@builder.io/qwik';
export default component$(() => {
const handleClick$ = $(() => {
console.log('Button clicked!');
});
return <button onClick$={handleClick$}>Click Me</button>;
});Qwik serializes:
- Event type (click)
- Handler location (chunk URL)
- Component state
- Closure data
The browser downloads the handler only when the button is clicked.
State Preservation
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const data = useSignal({
items: ['item1', 'item2'],
selected: null as string | null,
});
const selectItem$ = ((item: string) => {
data.value.selected = item;
});
return (
<div>
<For each={data.value.items}>
{(item) => (
<button onClick$={[selectItem$, item]}>
{item}
</button>
)}
</For>
<p>Selected: {data.value.selected}</p>
</div>
);
});The state is serialized and preserved across server/client boundary.
Benefits of Resumability
1. Instant Page Loads
Applications become interactive immediately without waiting for framework JavaScript:
<!-- HTML delivered with serialized state -->
<div id="app" data-qwik="...">
<span>Count: 0</span>
<button on:click="/build/handler-abc123.js">Increment</button>
</div>2. Minimal JavaScript
Only download what you need, when you need it:
// Main bundle: ~5KB
// Counter component: ~2KB
// Click handler: ~1KB (loaded only when clicked)3. Better Core Web Vitals
- LCP (Largest Contentful Paint): Fast initial paint
- FCP (First Contentful Paint): Immediate content display
- TTI (Time to Interactive): Instant interactivity
- TBT (Total Blocking Time): Minimal main thread blocking
Resumability in Practice
E-commerce Product Page
import { component$, useSignal, useResource$ } from '@builder.io/qwik';
export default component$(({ productId }: { productId: string }) => {
const showReviews = useSignal(false);
const addToCartLoading = useSignal(false);
const product = useResource$(({ track }) => {
track(() => productId);
return fetch(`/api/products/${productId}`).then(r => r.json());
});
return (
<div>
<Resource value={product}>
{(product) => (
<>
<h1>{product.name}</h1>
<p>{product.price}</p>
<button onClick$={async () => {
addToCartLoading.value = true;
await fetch(`/api/cart/add`, {
method: 'POST',
body: JSON.stringify({ productId: product.id }),
});
addToCartLoading.value = false;
}}>
{addToCartLoading.value ? 'Adding...' : 'Add to Cart'}
</button>
<button onClick$={() => showReviews.value = true}>
Show Reviews
</button>
{showReviews.value && (
<div>
{/* Reviews loaded only when requested */}
<ProductReviews productId={productId} />
</div>
)}
</>
)}
</Resource>
</div>
);
});Blog Post with Comments
import { component$, useSignal, useResource$ } from '@builder.io/qwik';
export default component$(({ slug }: { slug: string }) => {
const showComments = useSignal(false);
const post = useResource$(() => {
return fetch(`/api/posts/${slug}`).then(r => r.json());
});
return (
<article>
<Resource value={post}>
{(post) => (
<>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={post.content} />
<button onClick$={() => showComments.value = !showComments.value}>
{showComments.value ? 'Hide' : 'Show'} Comments
</button>
{showComments.value && (
<div>
{/* Comments section loaded on demand */}
<Comments postId={post.id} />
</div>
)}
</>
)}
</Resource>
</article>
);
});Progressive Enhancement
Progressive Loading of Features
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const activeTab = useSignal('overview');
return (
<div>
<nav>
<button onClick$={() => activeTab.value = 'overview'}>Overview</button>
<button onClick$={() => activeTab.value = 'features'}>Features</button>
<button onClick$={() => activeTab.value = 'pricing'}>Pricing</button>
</nav>
<div class="content">
{activeTab.value === 'overview' && (
<OverviewTab /> // Loaded when tab clicked
)}
{activeTab.value === 'features' && (
<FeaturesTab /> // Loaded when tab clicked
)}
{activeTab.value === 'pricing' && (
<PricingTab /> // Loaded when tab clicked
)}
</div>
</div>
);
});Qwik vs Traditional Frameworks
Bundle Size Comparison
React App:
- Initial bundle: ~150KB
- Hydration: ~50KB execution time
- Total JS: ~200KB
Qwik App:
- Initial bundle: ~10KB
- No hydration
- Progressive loading: ~5KB per interaction
Performance Metrics
| Metric | React | Qwik |
|---|---|---|
| Time to Interactive | 3-5s | <1s |
| First Contentful Paint | 1-2s | <1s |
| Total Blocking Time | 300-500ms | <50ms |
| Cumulative Layout Shift | 0.1 | 0.05 |
Best Practices for Resumability
1. Use component$ and Lazy Loading
// ✅ Good - Lazy loaded
export const ExpensiveChart = component$(() => {
return <Chart data={largeDataSet} />;
});
// ❌ Bad - Eagerly loaded
export const ExpensiveChart = () => {
return <Chart data={largeDataSet} />;
};2. Lazy Load Event Handlers
// ✅ Good - Handler lazy loaded
<button onClick$={() => console.log('clicked')}>Click</button>
// ❌ Bad - Handler eagerly loaded
<button onClick={() => console.log('clicked')}>Click</button>3. Use routeLoader$ for Data
// ✅ Good - Server-side data loading
export const useData = routeLoader$(async () => {
return fetch('/api/data').then(r => r.json());
});
// ❌ Bad - Client-side data loading
export default component$(() => {
const [data] = createResource(() => fetch('/api/data').then(r => r.json()));
});4. Prefer Server Actions
// ✅ Good - Server action
export const action = routeAction$(async (data) => {
return await saveToDatabase(data);
});
// ❌ Bad - Client-side fetch
const handleClick$ = $(async () => {
await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) });
});Debugging Resumability
Inspect Serialized State
<!-- View page source to see serialized state -->
<div q:base="/build/" q:container="paused">
<!-- Application state is serialized here -->
<script type="qwik/json">
{
"state": { "count": 0 },
"objs": [],
"ctxs": {},
"funcs": []
}
</script>
</div>Network Analysis
Use browser DevTools to see lazy loading in action:
- 1Open Network tab
- 2Filter for ".js" files
- 3Interact with the application
- 4Watch files load on-demand
Conclusion
Resumability is not just a performance optimization—it's a fundamental rethinking of how web applications should be delivered. By eliminating hydration and embracing lazy loading, Qwik achieves performance that was previously impossible.
The result is instant page loads, minimal JavaScript, and exceptional user experiences. As web applications grow in complexity, resumability becomes increasingly important for maintaining performance.
Qwik's resumability represents the future of web development, where the boundary between server and client disappears, and applications truly feel instant regardless of their complexity.