Introduction
Qwik is a revolutionary web framework that eliminates the need for hydration entirely. Instead, it uses “resumability” - the ability to pause execution on the server and resume on the client without re-running code. This guide covers Qwik’s unique approach and how to build blazing-fast applications.
Understanding Qwik
What is Resumability?
Traditional frameworks use “hydration” - re-running all JavaScript on the client to make HTML interactive. Qwik skips this entirely.
graph TB
subgraph "React/Next.js - Hydration"
Server1[Server: Render HTML]
Download[Download JS Bundle]
Execute[Execute All JS]
Interactive[Become Interactive]
Server1 --> Download
Download --> Execute
Execute --> Interactive
end
subgraph "Qwik - Resumability"
Server2[Server: Serialize State]
Download2[Download Minimal JS]
Resume[Resume from State]
Interactive2[Already Interactive]
Server2 --> Download2
Download2 --> Resume
Resume --> Interactive2
end
Key Concepts
| Concept | Description |
|---|---|
| Resumability | Pause on server, resume on client |
| Fine-grained Lazy Loading | Load JS only when needed |
| Zero Hydration | No JS execution on load |
| O(1) Startup | Constant time regardless of app size |
| Serialization | State serialized in HTML |
Getting Started
Installation
# Create new Qwik app
npm create qwik@latest my-app
# Add to existing project
npm run qwik add
Basic Component
// src/components/counter.tsx
import { component$, useSignal } from '@builder.io/qwik';
export const Counter = component$(() => {
const count = useSignal(0);
return (
<button onClick$={() => count.value++}>
Count: {count.value}
</button>
);
});
How It Works
// Qwik serializes this:
// <button q:target="counter_0">Count: 0</button>
// When clicked, loads only:
// http://localhost:5173/build/counter.js?hash=abc
// And executes just that tiny chunk!
Core API
Signals
import { component$, useSignal, useStore } from '@builder.io/qwik';
// Primitive signal
const count = useSignal(0);
count.value;
// Object signal
const state = useStore({ name: 'John', age: 30 });
state.name = 'Jane';
useTask$
import { component$, useTask$ } from '@builder.io/qwik';
export const DataFetcher = component$(() => {
const data = useSignal(null);
useTask$(async ({ track }) => {
track(() => data.value);
const res = await fetch('/api/data');
data.value = await res.json();
});
return <div>{JSON.stringify(data.value)}</div>;
});
Computed Values
import { component$, useComputed$ } from '@builder.io/qwik';
export const Calculator = component$(() => {
const a = useSignal(10);
const b = useSignal(5);
const sum = useComputed$(() => a.value + b.value);
return (
<div>
<input type="number" bind:value={a} />
<input type="number" bind:value={b} />
<p>Sum: {sum.value}</p>
</div>
);
});
Routing
File-Based Routing
src/routes/
โโโ index.tsx # /
โโโ about/
โ โโโ index.tsx # /about
โโโ blog/
โ โโโ index.tsx # /blog
โ โโโ [slug]/
โ โโโ index.tsx # /blog/:slug
โโโ layout.tsx # Shared layout
Route Loader
// 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 post = await db.posts.find(params.slug);
return post;
});
export default component$(() => {
const post = useBlogPost();
return (
<article>
<h1>{post.value.title}</h1>
<div>{post.value.content}</div>
</article>
);
});
Qwik City
Actions
import { component$ } from '@builder.io/qwik';
import { routeAction$, Form } from '@builder.io/qwik-city';
export const useSubmitForm = routeAction$(async (data) => {
await db.createUser(data);
return { success: true };
});
export default component$(() => {
const action = useSubmitForm();
return (
<Form action={action}>
<input name="email" type="email" />
<button type="submit">Submit</button>
</Form>
);
});
Performance
Lazy Loading Example
import { component$, type QRL } from '@builder.io/qwik';
// This function becomes a separate chunk
export const heavyComputation$: QRL<() => number> = $(() => {
// Only loads when called
return Math.random() * 100;
});
export const MyComponent = component$(() => {
return (
<button onClick$={async () => {
const result = await heavyComputation$();
console.log(result);
}}>
Calculate
</button>
);
});
Optimizer
// Qwik automatically splits:
// - Each component$: into separate chunk
// - Each $(...) lazy-loaded function
// - Only loads what's needed
// Result: 0KB JS on initial load!
// User clicks -> loads 1KB for that interaction
Qwik vs React
| Aspect | React | Qwik |
|---|---|---|
| Initial JS | 100KB+ | 0-1KB |
| Hydration | Required | None |
| Startup | O(n) | O(1) |
| Lazy Loading | Component-level | Function-level |
| Learning Curve | Moderate | Easy |
| Ecosystem | Large | Growing |
Conclusion
Qwik represents the future of web frameworks:
- Instant interactivity - no hydration needed
- O(1) startup - doesn’t scale with app size
- Fine-grained code splitting - load only what’s needed
- Familiar patterns - similar to React
Perfect for: performance-critical sites, e-commerce, content sites.
Comments