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: '[email protected]',
});
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](/en/blog/nextjs-15-complete-guide/
- Learn [Astro](/en/blog/astro-blog-complete-guide/
- Try [Solid.js](/en/blog/solid-js-complete-guide/
Resources:
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Complete Qwik guide for building instant-loading web apps. Learn resumability, fine-grained lazy loading, and zero-hydra… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- [Alpine.js Complete Guide | Lightweight JavaScript Framework](/en/blog/alpine-js-complete-guide/
- The Complete Qwik Guide | Resumability, Ultra-Fast Loading, Zero Hydration, Production Use
- [Next.js Complete Guide — App Router Internals, RSC, Caching](/en/blog/nextjs-complete-guide/
이 글에서 다루는 키워드 (관련 검색어)
Qwik, JavaScript, Performance, SSR, Resumability, Frontend 등으로 검색하시면 이 글이 도움이 됩니다.