Qwik is a revolutionary JavaScript framework that takes a different approach to web application performance. Instead of hydrating the entire application on load, Qwik uses a “resumable” model that delivers near-zero JavaScript to the client. This comprehensive guide covers everything you need to know about Qwik.
What is Qwik?
Qwik is a web framework that achieves instant interactivity through resumability - the ability to pause execution on the server and resume on the client without replaying all the application logic.
// Traditional React: Hydration loads all JavaScript
// User clicks button -> entire app hydrates -> event handler works
// Qwik: Event handler loads on-demand
// User clicks button -> only that handler's code loads -> works instantly
Core Philosophy
Qwik’s key innovations include:
- Resumability - No hydration, no replay, instant interactivity
- Lazy Loading - Code loads only when needed
- Fine-grained Reactivity - Update only what changes
- Zero Loading - Near-zero JavaScript on initial page load
- Server-First - Compute on server, ship HTML
flowchart LR
subgraph Traditional["Traditional Framework"]
T1[Server Render HTML] --> T2[Download JS Bundle]
T2 --> T3[Parse & Execute]
T3 --> T4[Hydrate App]
T4 --> T5[Interactive]
end
subgraph Qwik["Qwik Framework"]
Q1[Server Render HTML] --> Q2[Download Minimal JS]
Q2 --> Q3[No Hydration]
Q3 --> Q4[Instant Interactive]
end
T1 -.->|Same| Q1
T2 -.->|Much smaller| Q2
Installation and Setup
Creating a New Qwik Project
# Create a new Qwik app
npm create qwik@latest my-qwik-app
# Navigate to project
cd my-qwik-app
# Start development server
npm start
# Or use Qwik City for routing
npm create qwik@latest -- --template city
Project Structure
my-qwik-app/
โโโ src/
โ โโโ components/
โ โ โโโ counter/
โ โ โ โโโ counter.tsx
โ โ โโโ header/
โ โ โโโ header.tsx
โ โโโ routes/
โ โ โโโ index.tsx # Home page
โ โ โโโ about/
โ โ โ โโโ index.tsx # /about route
โ โ โโโ layout.tsx # Shared layout
โ โโโ entry.ssr.tsx # Server entry
โ โโโ entry.dev.tsx # Dev entry
โ โโโ root.tsx # Root component
โ โโโ global.css # Global styles
โโโ public/ # Static assets
โโโ package.json
โโโ vite.config.ts
Core Concepts
The $ Symbol
The $ symbol is fundamental to Qwik. It marks boundaries for lazy loading.
import { component$, useSignal, $ } from '@builder.io/qwik';
// Component is lazy loaded
export const Counter = component$(() => {
const count = useSignal(0);
// This function is lazy loaded separately
const increment = $(() => {
count.value++;
});
return (
<button onClick$={increment}>
Count: {count.value}
</button>
);
});
// Without $: function is inlined (not lazy loaded)
export const InlineExample = component$(() => {
const handleClick = () => {
console.log('This is inlined');
};
return <button onClick$={handleClick}>Click</button>;
});
useSignal: Fine-Grained Reactivity
import { component$, useSignal, useStore } from '@builder.io/qwik';
export const SignalExample = component$(() => {
// Primitive signal - updates only this text
const name = useSignal('World');
return (
<div>
<input
value={name.value}
onInput$={(e) => name.value = (e.target as HTMLInputElement).value}
/>
<p>Hello {name.value}!</p>
</div>
);
});
// Object store for nested reactivity
export const StoreExample = component$(() => {
const state = useStore({
user: { name: 'John', age: 30 },
items: ['item1', 'item2']
});
return (
<div>
<p>{state.user.name}</p>
<button onClick$={() => state.user.name = 'Jane'}>
Update Name
</button>
</div>
);
});
useStore: Deep Reactivity
import { component$, useStore } from '@builder.io/qwik';
export const DeepStore = component$(() => {
const store = useStore({
nested: {
deep: {
value: 'initial'
},
array: [{ id: 1, name: 'Item 1' }]
}
}, { deep: true }); // Enable deep reactivity
return (
<div>
<p>{store.nested.deep.value}</p>
<button onClick$={() => {
store.nested.deep.value = 'updated';
}}>
Update
</button>
</div>
);
});
Component Patterns
Basic Components
import { component$ } from '@builder.io/qwik';
interface Props {
title: string;
count?: number;
}
export const MyComponent = component$<Props>((props) => {
return (
<div class="my-component">
<h1>{props.title}</h1>
<p>Count: {props.count ?? 0}</p>
</div>
);
});
Components with Children
import { component$, Slot } from '@builder.io/qwik';
export const Card = component$(() => {
return (
<div class="card">
<div class="card-header">
<Slot name="header" />
</div>
<div class="card-body">
<Slot />
</div>
</div>
);
});
// Usage
export const Usage = component$(() => {
return (
<Card>
<span q:slot="header">Card Title</span>
<p>Card content goes here</p>
</Card>
);
});
Conditional Rendering
import { component$, useSignal } from '@builder.io/qwik';
export const ConditionalExample = component$(() => {
const show = useSignal(true);
return (
<div>
<button onClick$={() => show.value = !show.value}>
Toggle
</button>
{show.value && (
<p>This is conditionally rendered</p>
)}
{/* Or use ternary */}
{show.value ? (
<p>Showing</p>
) : (
<p>Hidden</p>
)}
</div>
);
});
List Rendering
import { component$, useStore } from '@builder.io/qwik';
interface Item {
id: number;
name: string;
}
export const ListExample = component$(() => {
const state = useStore<{ items: Item[] }>({
items: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
]
});
return (
<ul>
{state.items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
});
Event Handling
Click Events
import { component$, useSignal } from '@builder.io/qwik';
export const ClickHandler = component$(() => {
const clicked = useSignal(false);
return (
<button
onClick$={() => {
clicked.value = true;
console.log('Button clicked!');
}}
>
{clicked.value ? 'Clicked!' : 'Click Me'}
</button>
);
});
Input Events
import { component$, useSignal, $ } from '@builder.io/qwik';
export const InputHandler = component$(() => {
const text = useSignal('');
const handleInput = $((e: Event) => {
const target = e.target as HTMLInputElement;
text.value = target.value;
});
const handleSubmit = $(() => {
alert(`Submitted: ${text.value}`);
});
return (
<form preventdefault:submit onSubmit$={handleSubmit}>
<input
value={text.value}
onInput$={handleInput}
/>
<p>You typed: {text.value}</p>
<button type="submit">Submit</button>
</form>
);
});
Window/Document Events
import { component$, useVisibleTask$, useSignal } from '@builder.io/qwik';
export const WindowEvents = component$(() => {
const scrollY = useSignal(0);
useVisibleTask$(() => {
const handleScroll = () => {
scrollY.value = window.scrollY;
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
});
return (
<div>
<p>Scroll Y: {scrollY.value}</p>
</div>
);
});
Data Fetching
routeLoader$: Server Data Loading
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
// Server-side data loading
export const useUserLoader = routeLoader$(async (requestEvent) => {
// This runs on the server
const userId = requestEvent.params.id;
const response = await fetch(`https://api.example.com/users/${userId}`);
const user = await response.json();
return user;
});
export default component$(() => {
// Use the loader
const user = useUserLoader();
return (
<div>
<h1>{user.value.name}</h1>
<p>{user.value.email}</p>
</div>
);
});
server$: Server Actions
import { component$, $ } from '@builder.io/qwik';
import { server$ } from '@builder.io/qwik-city';
// Define server function
const serverAddTodo = server$(async (text: string) => {
// This runs on the server
const todo = await db.todo.create({ text });
return todo;
});
export const AddTodo = component$(() => {
return (
<button onClick$={async () => {
const newTodo = await serverAddTodo('New task');
console.log('Created:', newTodo);
}}>
Add Todo
</button>
);
});
Form Actions
import { component$ } from '@builder.io/qwik';
import { routeAction$, Form } from '@builder.io/qwik-city';
export const useAddComment = routeAction$(async (data) => {
// Handle form submission on server
await db.comment.create({
text: data.text,
postId: data.postId
});
return { success: true };
});
export const CommentForm = component$(() => {
const addComment = useAddComment();
return (
<Form action={addComment}>
<input name="text" placeholder="Add a comment" />
<input type="hidden" name="postId" value="123" />
<button type="submit">Submit</button>
</Form>
);
});
State Management
useContext: Sharing State
import { component$, useSignal, useContextProvider, useContext, createContextId } from '@builder.io/qwik';
// Create context
export const ThemeContext = createContextId<string>('theme-context');
export const App = component$(() => {
const theme = useSignal('dark');
// Provide context
useContextProvider(ThemeContext, theme);
return <Child />;
});
export const Child = component$(() => {
// Consume context
const theme = useContext(ThemeContext);
return (
<button onClick$={() => theme.value = theme.value === 'dark' ? 'light' : 'dark'}>
Current: {theme.value}
</button>
);
});
useTask$: Side Effects
import { component$, useSignal, useTask$ } from '@builder.io/qwik';
export const TaskExample = component$(() => {
const count = useSignal(0);
const doubled = useSignal(0);
// Runs when count changes
useTask$(({ track }) => {
track(() => count.value);
doubled.value = count.value * 2;
});
return (
<div>
<p>Count: {count.value}</p>
<p>Doubled: {doubled.value}</p>
<button onClick$={() => count.value++}>Increment</button>
</div>
);
});
Qwik City: Routing
Basic Routing
// src/routes/index.tsx
import { component$ } from '@builder.io/qwik';
import { Link } from '@builder.io/qwik-city';
export default component$(() => {
return (
<div>
<h1>Welcome</h1>
<Link href="/about">About</Link>
<Link href="/blog/post-1">Blog Post</Link>
</div>
);
});
Dynamic Routes
// src/routes/blog/[slug]/index.tsx
import { component$ } from '@builder.io/qwik';
import { useLocation, 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 loc = useLocation();
const post = useBlogPost();
return (
<article>
<h1>{post.value.title}</h1>
<p>Slug: {loc.params.slug}</p>
</article>
);
});
Nested Layouts
// src/routes/layout.tsx - Main layout
import { component$, Slot } from '@builder.io/qwik';
import { Link } from '@builder.io/qwik-city';
export default component$(() => {
return (
<div>
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
</nav>
<main>
<Slot />
</main>
</div>
);
});
// src/routes/dashboard/layout.tsx - Dashboard layout
import { component$, Slot } from '@builder.io/qwik';
export default component$(() => {
return (
<div class="dashboard">
<aside>
<Link href="/dashboard">Overview</Link>
<Link href="/dashboard/settings">Settings</Link>
</aside>
<main>
<Slot />
</main>
</div>
);
});
Middleware and Hooks
// src/routes/[email protected]
import type { RequestHandler } from '@builder.io/qwik-city';
export const onGet: RequestHandler = async ({ cookie, redirect }) => {
const session = cookie.get('session');
if (!session) {
throw redirect(302, '/login');
}
};
// Use in any route
// src/routes/admin/index.ts
import type { RequestHandler } from '@builder.io/qwik-city';
export const onGet: RequestHandler = async ({ redirect }) => {
// This route is protected
throw redirect(302, '/');
};
Performance Optimization
Code Splitting by Default
// Every component is automatically code-split
import { component$ } from '@builder.io/qwik';
// This component loads in a separate chunk
export const HeavyComponent = component$(() => {
return <div>I'm lazily loaded!</div>;
});
// Usage
export const Parent = component$(() => {
const show = useSignal(false);
return (
<div>
<button onClick$={() => show.value = !show.value}>
Toggle
</button>
{show.value && <HeavyComponent />}
</div>
);
});
Optimizing Images
import { component$ } from '@builder.io/qwik';
import { Image } from '@unpic/qwik';
export const OptimizedImage = component$(() => {
return (
<Image
src="/large-image.jpg"
layout="constrained"
width={800}
height={600}
alt="Description"
/>
);
});
Prefetching
import { component$ } from '@builder.io/qwik';
import { Link } from '@builder.io/qwik-city';
// Qwik automatically prefetches on hover
export const FastLink = component$(() => {
return (
<Link href="/heavy-page">
Load Heavy Page
</Link>
);
});
Comparison with Other Frameworks
| Feature | Qwik | React | Vue | Svelte |
|---|---|---|---|---|
| Hydration | None | Full | Full | Full |
| Initial JS | ~1KB | 40KB+ | 30KB+ | ~10KB |
| Learning Curve | Low | Medium | Low | Low |
| Reactivity | Fine-grained | Hooks-based | Reactive | Compiler-based |
| Server Components | Native | Next.js | Nuxt | SvelteKit |
| Ecosystem | Growing | Large | Large | Growing |
flowchart LR
subgraph Loading["JavaScript Loading"]
R[React] --> RBundle[40KB+]
RBundle --> RHydrate[Hydration]
RHydrate --> RReady[Interactive]
V[Vue] --> VBundle[30KB+]
VBundle --> VHydrate[Hydration]
VHydrate --> VReady[Interactive]
Q[Qwik] --> QBundle[~1KB]
QBundle --> QReady[Instant Interactive]
end
Best Practices
Do: Use $ Boundaries Correctly
// Good: $ marks lazy loading boundary
export const Good = component$(() => {
const handleClick = $(() => {
console.log('clicked');
});
return <button onClick$={handleClick}>Click</button>;
});
// Bad: Missing $ means inlining
export const Bad = component$(() => {
const handleClick = () => { // No $
console.log('clicked');
};
return <button onClick$={handleClick}>Click</button>;
});
Do: Use Proper Types
import { component$, type QRL } from '@builder.io/qwik';
interface ButtonProps {
onClick$: QRL<() => void>;
label: string;
}
export const Button = component$<ButtonProps>(({ onClick$, label }) => {
return <button onClick$={onClick$}>{label}</button>;
});
Don’t: Mutate Props Directly
// Bad: Mutating props
export const BadComponent = component$<{ count: number }>(({ count }) => {
count++; // Don't do this!
return <div>{count}</div>;
});
// Good: Use signals
export const GoodComponent = component$(() => {
const count = useSignal(0);
return <div>{count.value}</div>;
});
Deployment
Vercel
npm run qwik add vercel
// vercel.json
{
"functions": {
"api/*.js": {
"runtime": "@vercel/node"
}
}
}
Netlify
npm run qwik add netlify
Cloudflare Pages
npm run qwik add cloudflare-pages
External Resources
- Qwik Official Documentation
- Qwik GitHub Repository
- Qwik City Documentation
- Misko Hevery (Qwik Creator) Blog
- Qwik Discord Community
Conclusion
Qwik represents a paradigm shift in how we think about web application performance. By eliminating hydration entirely and using a resumable model, Qwik achieves near-instant interactivity regardless of application complexity.
The framework is ideal for:
- Performance-critical applications
- E-commerce sites
- Content-heavy websites
- Progressive web applications
- Any project where initial load time matters
While the ecosystem is younger than React or Vue, Qwik’s innovative approach makes it a compelling choice for teams prioritizing user experience and performance.
Comments