Qwik Tutorial: Learn Resumable Apps from Scratch (2026)
Sandip Mhaske • Ayodhyya • web-dev
Qwik Tutorial: Learn Resumable Apps from Scratch (2026)
Published: |
Category: Web Dev |
Reading time: ~15 min
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.
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.
);
});
// 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.
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 (
);
});
Styling and DX Features
Qwik supports scoped CSS via CSS modules, global CSS imports, and inline styles. The Qwik City starter includes Tailwind CSS integration out of the box. Styles are code-split along with components — only the CSS for components visible on the current page is loaded.
Developer experience features include Hot Module Replacement with near-instant updates, TypeScript support with strict mode, Vite-based tooling, and the Qwik Inspector browser extension for debugging component boundaries and chunk sizes.
// CSS Modules (scoped by default)
import styles from './card.module.css';
export default component$(() => {
return