web-dev5 min read

Qwik Tutorial: Learn Resumable Apps from Scratch (2026)

Qwik Tutorial: Learn Resumable Apps from Scratch (2026)

Published:  |  Category: Web Dev  |  Reading time: ~15 min
Qwik Tutorial: Learn Resumable Apps from Scratch (2026)

Qwik is a web framework focused on instant loading applications through resumability rather than hydration. Created by Miško Hevery (creator of Angular), Qwik delivers near-instant startup by serializing application state into HTML and resuming execution on the client — no JavaScript needs to run before the page becomes interactive.

Qwik’s secret is its fine-grained lazy loading: the framework automatically splits your application into tiny chunks, downloading only the code needed for the current user interaction. Combined with its Optimizer, Qwik achieves consistent sub-100ms Time to Interactive regardless of application size.

Resumability vs Hydration

Traditional frameworks like Next.js and Nuxt send static HTML then re-run all component code on the client (hydration). Qwik skips this entirely — it serializes the application state and event listeners into the HTML, then resumes execution exactly where the server left off. The client picks up the existing DOM and attaches listeners without re-executing component logic.

This means zero execution during page load. Qwik only downloads and executes the code for the specific event the user triggered. A button click downloads only that button’s handler, not the entire page bundle.

// Qwik serializes state into HTML automatically
// Server-rendered HTML includes state as 
// The component code is NOT downloaded until needed // Only when user clicks does Qwik fetch the handler export const handleClick = $(() => { console.log('Button clicked'); }); // Without resumability, all this code would need to run: // 1. Parse all components // 2. Execute all effects // 3. Reconcile virtual DOM // With Qwik: nothing runs until interaction

The Optimizer and Fine-Grained Lazy Loading

Qwik’s Optimizer is a build-time tool that automatically extracts closures, expressions, and event handlers into separate lazy-loadable chunks. You don’t decide what to lazy load — the Optimizer does it automatically by analyzing the dependency graph. The $() suffix marks reactive boundaries for code splitting.

The optimizer ensures that only code reachable from a specific event handler is included in that chunk. If a handler calls a utility function, that function is inlined or extracted into the same chunk. This creates a granular loading pattern where every interaction downloads exactly what it needs.

import { component$, useSignal, $ } from '@builder.io/qwik';

// component$ marks this for lazy loading
export default component$(() => {
  const count = useSignal(0);

  // $() extracts the handler into its own chunk
  const increment = $(() => {
    count.value++;
  });

  return (
    
  );
});

// The optimizer 'serializes' the closure
// It sees count.value is accessed, so it includes
// the setter for count in the handler's chunk

// Without $() (not lazy loaded):
export default function HeavyComponent() {
  // This entire component always runs
  return 
Always loaded
; } // With $() (lazy): export default component$(() => { // Only loaded when this component appears in viewport return
Lazy when needed
; });

useSignal and Reactive State

useSignal and useStore are Qwik’s reactive primitives. Signals hold primitive values with a .value property that tracks reads and writes. Stores hold deeply reactive objects and arrays. Unlike React’s useState, Qwik’s signals can be serialized to HTML and restored across server-client boundaries without re-execution.

Tracked reads happen automatically — the Qwik runtime knows which components access which signals. When a signal changes, only the DOM nodes depending on that signal are updated. This fine-grained reactivity means no virtual DOM and no component re-renders.

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

export default component$(() => {
  const count = useSignal(0);
  const user = useStore({ name: 'Alice', todos: [] });

  const doubled = useComputed$(() => count.value * 2);

  return (
    

Count: {count.value}

Doubled: {doubled.value}

User: {user.name}

); }); // Signals are serializable // On server: count = 5 // HTML includes serialized state // Client resumes with count = 5, no re-execution const count = useSignal(0);

Routing and Layouts

Qwik City is Qwik’s meta-framework providing file-based routing, layouts, and data loading. Routes are defined by the src/routes directory structure with index.tsx, [param]/index.tsx, and [...catchall]/index.tsx. routeLoader$ loads data on the server, which is then serialized and resumed on the client.

Layout files wrap nested routes. The component renders child route content. Qwik City supports middleware, route guards, and endpoint handlers for API routes. Each page can declare server-side data requirements.

// src/routes/layout.tsx
import { component$, Slot } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';

export const useServerData = routeLoader$(async () => {
  const res = await fetch('https://api.example.com/config');
  return res.json();
});

export default component$(() => {
  return (
    
  );
});

// src/routes/blog/[slug]/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';

export const useBlogPost = routeLoader$(async ({ params }) => {
  const res = await fetch(`/api/posts/${params.slug}`);
  return res.json();
});

export default component$(() => {
  const post = useBlogPost();
  return 

{post.value.title}

; });

Server Functions and Actions

Server functions defined with server$() run exclusively on the server but can be called from client code. They handle form submissions, authentication, database mutations, and any server-only logic. Qwik automatically generates the RPC call and handles serialization.

Form actions with action$() provide progressive enhancement — forms work without JavaScript and submit via Fetch when JS loads. Qwik bundles the form handler into a lazy chunk downloaded only when the form is submitted.

import { component$ } from '@builder.io/qwik';
import { routeAction$, server$ } from '@builder.io/qwik-city';

// Server-only function
const encryptData = server$(async (text) => {
  // This code NEVER runs on the client
  const crypto = await import('node:crypto');
  return crypto.createHash('sha256').update(text).digest('hex');
});

// Form action with validation
export const useCreatePost = routeAction$(async (form, { fail }) => {
  const { title, body } = form;
  if (title.length < 3) {
    return fail(400, { message: 'Title too short' });
  }
  const post = await db.posts.create({ title, body });
  return { success: true, postId: post.id };
});

// Component using the action
export default component$(() => {
  const createPost = useCreatePost();

  return (