React hydrates. Qwik resumes. The difference? Your app is interactive instantly — no matter how large.
The Problem with Hydration
Every framework does this:
- Server renders HTML
- Client downloads JavaScript
- Client re-executes everything to make it interactive (hydration)
For large apps, step 3 can take 5-10 seconds. Qwik skips it entirely.
How Resumability Works
Qwik serializes the app's state and event listeners into the HTML. When the browser loads the page, it already knows what to do — no re-execution needed.
Traditional: Download All JS → Execute All → Interactive
Qwik: HTML loads → Already Interactive → Load JS on demand
Quick Start
npm create qwik@latest
cd my-app && npm start
Basic Component
import { component$, useSignal } from '@builder.io/qwik';
export const Counter = component$(() => {
const count = useSignal(0);
return (
<div>
<p>Count: {count.value}</p>
<button onClick$={() => count.value++}>Increment</button>
</div>
);
});
Notice the $ signs — they tell Qwik where to create lazy-loading boundaries.
Lazy Loading by Default
import { component$, useTask$ } from '@builder.io/qwik';
export const UserProfile = component$(() => {
const user = useSignal<User | null>(null);
// This code ONLY loads when it needs to run
useTask$(async () => {
const res = await fetch('/api/user');
user.value = await res.json();
});
return (
<div>
{user.value ? (
<div>
<h2>{user.value.name}</h2>
<p>{user.value.email}</p>
{/* onClick handler JS only loads when user clicks */}
<button onClick$={() => {
// This entire handler is a separate chunk
// It only downloads when the button is clicked
fetch('/api/logout', { method: 'POST' });
}}>Logout</button>
</div>
) : (
<p>Loading...</p>
)}
</div>
);
});
useStore (Reactive Objects)
import { component$, useStore } from '@builder.io/qwik';
export const TodoApp = component$(() => {
const state = useStore({
todos: [] as Array<{ text: string; done: boolean }>,
newTodo: '',
});
return (
<div>
<input
value={state.newTodo}
onInput$={(e) => state.newTodo = (e.target as HTMLInputElement).value}
/>
<button onClick$={() => {
state.todos.push({ text: state.newTodo, done: false });
state.newTodo = '';
}}>Add</button>
<ul>
{state.todos.map((todo, i) => (
<li key={i}>
<input
type="checkbox"
checked={todo.done}
onChange$={() => todo.done = !todo.done}
/>
{todo.text}
</li>
))}
</ul>
</div>
);
});
Server Actions (Qwik City)
import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city';
export const useCreateUser = routeAction$(
async (data) => {
const user = await db.users.create({ data });
return { success: true, userId: user.id };
},
zod$({
name: z.string().min(1),
email: z.string().email(),
})
);
export default component$(() => {
const action = useCreateUser();
return (
<Form action={action}>
<input name="name" placeholder="Name" />
<input name="email" placeholder="Email" />
<button type="submit">Create User</button>
{action.value?.success && <p>User created!</p>}
</Form>
);
});
Qwik vs Next.js vs Remix
| Feature | Qwik | Next.js | Remix |
|---|---|---|---|
| Initial JS | ~1KB | 80-200KB | 50-150KB |
| TTI | Instant | Seconds | Seconds |
| Hydration | None (resumable) | Full | Full |
| Lazy Loading | Automatic | Manual | Manual |
| Bundle Growth | O(1) | O(n) | O(n) |
As your app grows, Qwik's initial load stays constant. Other frameworks grow linearly.
Need fast data loading for your Qwik app? Check out my Apify actors — data extraction that matches Qwik's speed. For custom solutions, email spinov001@gmail.com.
Top comments (0)