Skip to content
All essays
WebMarch 27, 202615 min

Qwik Resumability: The Future of Web Application Performance

Deep dive into Qwik resumability and how it eliminates hydration for instant page loads and optimal performance.

Ü
Ümit Uz
Mobile & Full Stack Developer

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:

  1. 1Server renders HTML
  2. 2Client downloads entire framework
  3. 3Framework executes to recreate component tree
  4. 4Event listeners are attached
  5. 5Application becomes interactive

Qwik Resumability:

  1. 1Server renders HTML
  2. 2Client downloads minimal JavaScript
  3. 3Application resumes immediately
  4. 4Event listeners loaded on-demand
  5. 5Instant interactivity

How Resumability Works

Serialization of State

Qwik serializes the application state during server-side rendering:

tsx
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:

  1. 1Renders the HTML
  2. 2Serializes component state
  3. 3Closures are captured and serialized
  4. 4Event listeners are registered in the HTML

Lazy Loading Everything

Qwik breaks everything into lazy-loadable chunks:

tsx
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

tsx
export const Counter = component$(() => {
  const count = useSignal(0);
  return <button onClick$={() => count.value++}>{count.value}</button>;
});

After Optimization

The Optimizer creates:

  1. 1A component chunk (loaded when component renders)
  2. 2An event handler chunk (loaded when button clicked)
  3. 3State serialization (included in HTML)

Event Listener Resumability

Event Listener Registration

tsx
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

tsx
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
<!-- 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:

tsx
// 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

tsx
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

tsx
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

tsx
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

MetricReactQwik
Time to Interactive3-5s<1s
First Contentful Paint1-2s<1s
Total Blocking Time300-500ms<50ms
Cumulative Layout Shift0.10.05

Best Practices for Resumability

1. Use component$ and Lazy Loading

tsx
// ✅ 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

tsx
// ✅ 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

tsx
// ✅ 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

tsx
// ✅ 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

html
<!-- 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:

  1. 1Open Network tab
  2. 2Filter for ".js" files
  3. 3Interact with the application
  4. 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.

Enjoyed this essay? Share it.

Related essays

Next essay
Qwik City Actions and Form Handling: Building Interactive Applications