Qwik Complete Guide | Resumable JavaScript Framework
이 글의 핵심
Qwik is a new framework that delivers instant-loading web apps through resumability. Unlike React/Vue, Qwik serializes app state on the server and resumes on the client without hydration.
Introduction
Qwik is a new kind of web framework that solves the hydration problem. Instead of shipping JavaScript to re-create the app state on the client, Qwik serializes the state on the server and resumes execution on the client.
Created by Miško Hevery (creator of Angular and AngularJS) and backed by Builder.io, Qwik represents a fundamental rethinking of how web frameworks should work. The core innovation — resumability — eliminates hydration entirely.
Why Qwik Matters
The hydration tax:
- Next.js/Remix apps ship 40-200KB JavaScript that runs immediately
- Users wait 1-3 seconds on mobile before the page becomes interactive
- Even static content requires JavaScript to “hydrate” and become interactive
Qwik’s solution:
- Zero JavaScript by default — only loads code when user interacts
- Instant interactivity — buttons work immediately, no hydration wait
- 1KB overhead vs 40KB+ for React/Vue
Real-world adoption:
- Builder.io (creators of Qwik) uses it for their visual development platform
- Qwik City (meta-framework) reached 1.0 in 2023
- Growing interest from enterprises tired of slow Time to Interactive (TTI)
- ~40k weekly npm downloads (rapid growth since 1.0 release)
Performance impact (real production metrics):
- Time to Interactive: <100ms (vs 1-3s for traditional SSR)
- Initial JavaScript: 1-5KB (vs 40-200KB for Next.js/Remix)
- Lighthouse scores: consistent 100 on Performance (hard to achieve with React)
When to use Qwik:
- E-commerce sites — every 100ms TTI improvement = 1% conversion rate increase
- Content-heavy sites — blogs, documentation where most content is static
- Mobile-first apps — slow networks benefit most from minimal JS
- SEO-critical pages — landing pages, marketing sites
When to use Next.js/Remix instead:
- Complex dashboards with lots of client-side state
- Need larger ecosystem and more libraries
- Team already expert in React patterns
- Using React Native (Qwik is web-only)
The Hydration Problem
Traditional SSR (React/Next.js):
- Server renders HTML
- Browser displays HTML (fast!)
- JavaScript downloads
- Hydration: React re-runs all components to attach listeners
- App becomes interactive (slow!)
Qwik’s Resumability:
- Server renders HTML + serialized state
- Browser displays HTML (fast!)
- App is immediately interactive
- JavaScript loads only when needed
Bundle Size Comparison
For a simple counter:
- React SSR: 40KB JavaScript (entire React runtime)
- Qwik: 1KB JavaScript (only event handler)
1. Installation
npm create qwik@latest
cd my-qwik-app
npm install
npm run dev
2. Basic Concepts
Components
import { component$ } from '@builder.io/qwik';
export const Counter = component$(() => {
return <div>Hello, Qwik!</div>;
});
Note the $ suffix: It tells Qwik optimizer to lazy-load this code.
State with useSignal
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>
);
});
Key differences from React:
useSignalinstead ofuseState- Access with
.value onClick$instead ofonClick(lazy-loaded!)
3. Event Handling
onClick$
export const Button = component$(() => {
const handleClick$ = $(() => {
console.log('Clicked!');
});
return <button onClick$={handleClick$}>Click me</button>;
});
Inline Handlers
export const Form = component$(() => {
const name = useSignal('');
return (
<input
value={name.value}
onInput$={(e) => name.value = e.target.value}
/>
);
});
4. useStore (Objects)
import { component$, useStore } from '@builder.io/qwik';
export const UserProfile = component$(() => {
const user = useStore({
name: 'John',
age: 30,
email: 'john@example.com',
});
return (
<div>
<input
value={user.name}
onInput$={(e) => user.name = e.target.value}
/>
<p>Age: {user.age}</p>
</div>
);
});
5. Async Data with routeLoader$
// routes/users/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
export const useUsers = routeLoader$(async () => {
const res = await fetch('https://api.example.com/users');
return res.json();
});
export default component$(() => {
const users = useUsers();
return (
<ul>
{users.value.map((user: any) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
});
Key points:
routeLoader$runs on the server- Data is serialized and sent to client
- No hydration needed!
6. Forms with routeAction$
import { component$ } from '@builder.io/qwik';
import { routeAction$, Form } from '@builder.io/qwik-city';
export const useAddUser = routeAction$(async (data) => {
// Runs on server
const res = await fetch('https://api.example.com/users', {
method: 'POST',
body: JSON.stringify(data),
});
return await res.json();
});
export default component$(() => {
const action = useAddUser();
return (
<Form action={action}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit">Add User</button>
{action.value?.success && <p>User added!</p>}
</Form>
);
});
7. Routing with Qwik City
File-based Routing
src/routes/
├── index.tsx → /
├── about.tsx → /about
├── blog/
│ ├── index.tsx → /blog
│ └── [slug].tsx → /blog/:slug
└── users/
└── [id]/
└── index.tsx → /users/:id
Dynamic Routes
// routes/blog/[slug].tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
export const usePost = routeLoader$(async ({ params }) => {
const res = await fetch(`/api/posts/${params.slug}`);
return res.json();
});
export default component$(() => {
const post = usePost();
return (
<article>
<h1>{post.value.title}</h1>
<div>{post.value.content}</div>
</article>
);
});
Navigation
import { component$ } from '@builder.io/qwik';
import { Link } from '@builder.io/qwik-city';
export const Nav = component$(() => {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog">Blog</Link>
</nav>
);
});
8. Lifecycle Hooks
useVisibleTask$
import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik';
export const Chart = component$(() => {
const chartRef = useSignal<HTMLDivElement>();
useVisibleTask$(({ track }) => {
track(() => chartRef.value);
// Runs when component becomes visible
if (chartRef.value) {
// Initialize chart library
initChart(chartRef.value);
}
});
return <div ref={chartRef}></div>;
});
useTask$
import { component$, useSignal, useTask$ } from '@builder.io/qwik';
export const AutoSave = component$(() => {
const text = useSignal('');
useTask$(({ track }) => {
track(() => text.value);
// Runs on server AND client when text changes
console.log('Text changed:', text.value);
});
return <input value={text.value} onInput$={(e) => text.value = e.target.value} />;
});
9. Context API
import { component$, createContextId, useContextProvider, useContext } from '@builder.io/qwik';
// Create context
export const UserContext = createContextId<{ name: string }>('user');
// Provider
export const App = component$(() => {
const user = useStore({ name: 'John' });
useContextProvider(UserContext, user);
return <UserProfile />;
});
// Consumer
export const UserProfile = component$(() => {
const user = useContext(UserContext);
return <div>{user.name}</div>;
});
10. Real-World Example: Todo App
import { component$, useStore } from '@builder.io/qwik';
import { routeLoader$, routeAction$, Form } from '@builder.io/qwik-city';
export const useTodos = routeLoader$(async () => {
// Load from database
return [
{ id: 1, text: 'Learn Qwik', done: false },
{ id: 2, text: 'Build app', done: false },
];
});
export const useAddTodo = routeAction$(async (data) => {
// Save to database
return { success: true };
});
export const useToggleTodo = routeAction$(async (data) => {
// Update in database
return { success: true };
});
export default component$(() => {
const todos = useTodos();
const addAction = useAddTodo();
const toggleAction = useToggleTodo();
return (
<div>
<h1>Todo App</h1>
<Form action={addAction}>
<input name="text" required />
<button type="submit">Add</button>
</Form>
<ul>
{todos.value.map((todo) => (
<li key={todo.id}>
<Form action={toggleAction}>
<input type="hidden" name="id" value={todo.id} />
<input
type="checkbox"
checked={todo.done}
onChange$={() => {
// Submit form on change
}}
/>
<span>{todo.text}</span>
</Form>
</li>
))}
</ul>
</div>
);
});
11. Performance Benefits
Zero JavaScript by Default
// This page ships ZERO JavaScript
export default component$(() => {
return (
<div>
<h1>Hello, World!</h1>
<p>Static content needs no JS!</p>
</div>
);
});
Fine-grained Lazy Loading
// Only this button's handler is loaded when clicked
export default component$(() => {
return (
<div>
<h1>Page content (no JS)</h1>
<button onClick$={() => console.log('Clicked')}>
Click me (1KB JS loaded on click)
</button>
</div>
);
});
12. Deployment
Cloudflare Pages
npm run build
wrangler pages publish dist
Vercel
npm run build
vercel deploy
Node.js
npm run build
npm run serve
13. Best Practices
1. Use $ for Lazy Loading
// Good: Lazy-loaded
const handleClick$ = $(() => console.log('Clicked'));
// Bad: Eagerly loaded
const handleClick = () => console.log('Clicked');
2. Use useSignal for Primitives
// Good
const count = useSignal(0);
// Unnecessary for primitives
const state = useStore({ count: 0 });
3. Prefer routeLoader$ over useTask$
// Good: Runs once on server
export const useData = routeLoader$(async () => {
return await fetchData();
});
// Bad: Runs on server AND client
useTask$(async () => {
const data = await fetchData();
});
Summary
Qwik delivers instant-loading apps through resumability:
- Zero hydration - app resumes, doesn’t restart
- Fine-grained lazy loading - load code only when needed
- Zero JavaScript by default - only ship what’s needed
- Instant interactivity - no waiting for hydration
- Optimized automatically -
$syntax enables magic
Key Takeaways:
- Use
$suffix for lazy-loading useSignalfor state,.valueto accessrouteLoader$for server datarouteAction$for forms- Zero JavaScript for static content
Next Steps:
- Compare with Next.js 15
- Learn Astro
- Try Solid.js
Resources:
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Complete Qwik guide for building instant-loading web apps.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- Alpine.js Complete Guide | Lightweight JavaScript Framework
- The Complete Qwik Guide | Resumability, Ultra-Fast Loading, Zero Hydration, Production Use
- Next.js Complete Guide — App Router Internals, RSC, Caching
Keywords Covered in This Article (Related Search Terms)
This article covers Qwik, JavaScript, Performance, SSR, Resumability, Frontend.