SalakCode SalakCode
Rendering

Resumability

Zero-hydration instant interactivity

Advanced
qwik hydration performance resumability

Definition

Resumability is a rendering paradigm pioneered by Qwik that eliminates the need for hydration. Instead of re-executing the application on the client to attach event listeners and rebuild state, the server serializes the entire application state into HTML. The browser “resumes” execution instantly without replaying component code.

The Hydration Problem

Traditional SSR Flow

Server:                    Client:
1. Execute app             1. Download HTML
2. Generate HTML           2. Download JS (500KB)
3. Send HTML               3. Parse JS
                           4. Execute JS (re-run app)
                           5. Build component tree
                           6. Attach event listeners
                           7. Page interactive
                           
Time to Interactive: 2-5 seconds

The Cost

// All this JavaScript must download and execute
import React from 'react';
import { Header, Footer, Sidebar, Content } from './components';
import { Router, StateProvider } from './lib';
import * as utils from './utils';
// ... 500KB of dependencies

function App() {
  // All component code runs twice (server + client)
  return (
    <StateProvider>
      <Router>
        <Header />
        <Sidebar />
        <Content />
        <Footer />
      </Router>
    </StateProvider>
  );
}

How Resumability Works

Serialization

// Server serializes:
{
  "state": {
    "counter": {
      "count": 5
    },
    "user": {
      "name": "John",
      "preferences": { "theme": "dark" }
    }
  },
  "listeners": [
    {
      "element": "button#increment",
      "event": "click",
      "qrl": "./counter#increment"
    }
  ],
  "refs": {
    "counter-component": { "id": "counter-1", "state": "counter" }
  }
}

HTML Output

<div q:container="paused">
  <div q:id="counter-1">
    <span>Count: 5</span>
    <button
      q:listener="click:./counter#increment"
      on:click="q-factory.js#handleEvent"
    >
      +
    </button>
  </div>
</div>

<script type="qwik/json">
{"state": {"counter-1": {"count": 5}}}
</script>

Lazy Loading

// No JavaScript loaded initially!
// Only when user clicks:

// 1. Download handler (1KB)
import { increment } from './counter.js';

// 2. Deserialize state
const state = { count: 5 };

// 3. Execute handler
increment(state); // count = 6

// 4. Update DOM
element.textContent = 'Count: 6';

Qwik Implementation

Components

// counter.tsx
import { component$, useStore } from '@builder.io/qwik';

export const Counter = component$(() => {
  const store = useStore({ count: 0 });
  
  return (
    <div>
      <span>Count: {store.count}</span>
      <button onClick$={() => store.count++}>
        +
      </button>
    </div>
  );
});

Lazy Boundaries

// The $ suffix creates lazy boundaries
// Code split at these points

onClick$={() => store.count++}  // Split here
useTask$(() => { ... })          // Split here  
useVisible$(() => { ... })       // Split when visible
useClientEffect$(() => { ... })  // Split, run on client

State Management

// useStore - Reactive state (serialized)
const store = useStore({ count: 0 });

// useSignal - Primitive values (serialized)
const count = useSignal(0);

// useContext - Shared state (serialized)
const theme = useContext(ThemeContext);

// useResource$ - Async data (resumable)
const userData = useResource$(async () => {
  return await fetchUser();
});

Performance Comparison

Bundle Sizes

Traditional SPA:
- Initial JS: 500KB
- Hydration: 200ms
- TTI: 3s

Qwik:
- Initial HTML: 50KB
- Initial JS: 1KB (framework)
- First interaction: Load 2KB handler
- TTI: <100ms

Progressive Loading

User visits page:
├─ HTML loads (50KB)
├─ Page is interactive immediately (no hydration)
├─ User clicks button
├─ Download 2KB handler
├─ Execute handler (deserialize state, update DOM)
└─ Done!

Only load what's needed, when needed

Resumability vs Islands

Islands Architecture

// Static HTML with hydrated islands
<article>
  Static content (no JS)
  <Island client:load>
    Interactive part (hydrated)
  </Island>
</article>

// Each island hydrates independently
// Still requires hydration for interactive parts

Resumability

// Everything can be interactive
// No hydration needed
<article>
  <div onClick$={handler}>  <!-- Lazy loaded on click -->
    Click me
  </div>
</article>

// State preserved from server
// Listeners attached via deserialization
// Code loaded on demand

Use Cases

E-commerce

// Product page loads instantly
// All interactive elements work immediately
// Add to cart: 2KB download
// Update quantity: 1KB download
// Complex interactions loaded progressively

export const ProductPage = component$(() => {
  const cart = useStore({ items: [] });
  
  return (
    <div>
      <ProductDetails />
      <AddToCartButton onAdd$={(product) => {
        cart.items.push(product);
      }} />
      <Reviews />
    </div>
  );
});

Content Sites

// Perfect for blogs, docs, marketing sites
// Instant load, progressive enhancement
// Search, filters, comments lazy loaded

export const BlogPost = component$(() => {
  return (
    <article>
      <Markdown content={post.content} />
      <LazySearch client:visible />
      <Comments client:idle postId={post.id} />
    </article>
  );
});

Trade-offs

Benefits

  • Zero hydration overhead
  • Instant interactivity
  • Minimal initial JS
  • Optimal caching (each symbol cached independently)
  • Progressive loading

Limitations

  • New paradigm (learning curve)
  • Ecosystem smaller than React/Vue
  • Requires Qwik-specific patterns
  • Not ideal for highly dynamic SPAs
Key Takeaway

Resumability eliminates hydration by serializing application state in HTML and lazy-loading code on demand. It provides instant interactivity with minimal JavaScript, making it ideal for content sites and e-commerce. While the paradigm differs from traditional frameworks, it solves the fundamental hydration performance problem.

Resources

Related Topics