Qwik - Instant Loading Web Applications
Czym jest Qwik?
Qwik to rewolucyjny framework JavaScript stworzony przez Miško Hevery (twórcę Angular) i zespół Builder.io. Wprowadza całkowicie nowe podejście do renderowania aplikacji webowych poprzez koncepcję "resumability" - zamiast tradycyjnej hydration, aplikacja Qwik jest interaktywna natychmiast po załadowaniu HTML, bez konieczności ładowania i wykonywania całego JavaScript z góry.
Framework rozwiązuje fundamentalny problem współczesnych aplikacji SPA: nawet przy SSR, użytkownik musi czekać na pobranie i wykonanie całego JS bundle, zanim strona stanie się interaktywna. Qwik eliminuje ten problem poprzez serializację stanu aplikacji bezpośrednio w HTML i lazy-loading JavaScript na poziomie pojedynczych event handlerów.
Problem: Hydration Tax
Jak działa tradycyjne SSR
W tradycyjnych frameworkach (React, Vue, Svelte) Server-Side Rendering przebiega następująco:
1. Serwer → Renderuje HTML
2. Przeglądarka → Pobiera HTML, wyświetla statyczną stronę
3. Przeglądarka → Pobiera CAŁY JavaScript bundle (50-500KB+)
4. Przeglądarka → Wykonuje JavaScript
5. Framework → "Hydratuje" stronę (re-renderuje wszystko w pamięci)
6. Strona → Staje się interaktywnaProblem: Kroki 3-5 to "hydration tax" - czas, który użytkownik musi czekać na interaktywność. Na wolnych urządzeniach mobilnych może to trwać kilka sekund.
Jak działa Qwik Resumability
1. Serwer → Renderuje HTML + serializuje stan w atrybutach
2. Przeglądarka → Pobiera HTML, wyświetla stronę
3. Strona → Jest NATYCHMIAST interaktywna
4. Przeglądarka → Pobiera JS tylko dla klikniętych elementów (lazy)Korzyść: Brak hydration = natychmiastowa interaktywność.
Qwik vs React/Vue - porównanie wydajności
| Metryka | Qwik | React | Vue | Angular |
|---|---|---|---|---|
| Initial JS | ~1KB | 50-100KB | 40-80KB | 80-150KB |
| Time to Interactive | Instant | 1-5s | 1-4s | 2-6s |
| Hydration | Brak (resume) | Tak | Tak | Tak |
| Lazy loading | Per-listener | Per-route | Per-route | Per-route |
| Bundle growth | Liniowy | Wykładniczy | Wykładniczy | Wykładniczy |
| SSR overhead | Minimalny | Wysoki | Średni | Wysoki |
Instalacja i konfiguracja
Tworzenie nowego projektu
# Inicjalizacja projektu z Qwik CLI
npm create qwik@latest
# Lub z pnpm
pnpm create qwik@latest
# Interaktywny wizard zapyta o:
# - Nazwę projektu
# - Starter template (podstawowy, z integrami)
# - Czy dodać Qwik City (routing/meta-framework)Struktura projektu Qwik City
my-qwik-app/
├── src/
│ ├── components/ # Komponenty Qwik
│ │ ├── header/
│ │ │ └── header.tsx
│ │ └── footer/
│ │ └── footer.tsx
│ ├── routes/ # File-based routing
│ │ ├── index.tsx # / (home page)
│ │ ├── about/
│ │ │ └── index.tsx # /about
│ │ ├── blog/
│ │ │ ├── index.tsx # /blog
│ │ │ └── [slug]/
│ │ │ └── index.tsx # /blog/:slug
│ │ └── layout.tsx # Shared layout
│ ├── entry.ssr.tsx # SSR entry point
│ └── root.tsx # Root component
├── public/ # Static assets
├── vite.config.ts # Vite configuration
├── qwik.config.ts # Qwik configuration
└── package.jsonPodstawowa konfiguracja
// vite.config.ts
import { defineConfig } from 'vite'
import { qwikVite } from '@builder.io/qwik/optimizer'
import { qwikCity } from '@builder.io/qwik-city/vite'
export default defineConfig(() => {
return {
plugins: [qwikCity(), qwikVite()],
server: {
port: 5173,
},
preview: {
port: 4173,
},
}
})Podstawy Qwik - składnia
Komponenty z component$
// components/counter.tsx
import { component$, useSignal } from '@builder.io/qwik'
// $ suffix oznacza, że funkcja jest lazy-loaded
export const Counter = component$(() => {
// useSignal - reaktywny stan (fine-grained reactivity)
const count = useSignal(0)
return (
<div class="counter">
<p>Count: {count.value}</p>
{/* onClick$ - handler lazy-loaded przy pierwszym kliknięciu */}
<button onClick$={() => count.value++}>
Increment
</button>
<button onClick$={() => count.value--}>
Decrement
</button>
</div>
)
})Znaczenie symbolu $ (Dollar Sign)
Symbol $ w Qwik oznacza granicę lazy-loadingu. Wszystko po $ jest serializowane i ładowane tylko gdy potrzebne:
import { component$, $, useSignal } from '@builder.io/qwik'
export const Example = component$(() => {
const message = useSignal('')
// $ tworzy lazy-loaded funkcję
const handleClick = $(() => {
// Ten kod jest w osobnym chunk i ładowany przy kliknięciu
message.value = 'Button clicked!'
console.log('This code was lazy-loaded')
})
// onClick$ automatycznie opakowuje w $
return (
<div>
<button onClick$={handleClick}>Click me</button>
<p>{message.value}</p>
</div>
)
})useSignal - reaktywny stan
import { component$, useSignal } from '@builder.io/qwik'
export const SignalExample = component$(() => {
// Prymitywne wartości
const count = useSignal(0)
const name = useSignal('John')
const isActive = useSignal(true)
// Zmiana wartości
const increment = $(() => {
count.value++
})
const updateName = $((newName: string) => {
name.value = newName
})
return (
<div>
<p>Count: {count.value}</p>
<p>Name: {name.value}</p>
<p>Active: {isActive.value ? 'Yes' : 'No'}</p>
<button onClick$={increment}>+1</button>
<input
value={name.value}
onInput$={(e) => name.value = (e.target as HTMLInputElement).value}
/>
<button onClick$={() => isActive.value = !isActive.value}>
Toggle
</button>
</div>
)
})useStore - reaktywne obiekty
import { component$, useStore } from '@builder.io/qwik'
interface TodoItem {
id: number
text: string
completed: boolean
}
interface State {
todos: TodoItem[]
filter: 'all' | 'active' | 'completed'
newTodo: string
}
export const TodoApp = component$(() => {
// useStore dla złożonych obiektów - głęboka reaktywność
const state = useStore<State>({
todos: [],
filter: 'all',
newTodo: '',
})
const addTodo = $(() => {
if (state.newTodo.trim()) {
// Bezpośrednia mutacja - reaktywna!
state.todos.push({
id: Date.now(),
text: state.newTodo,
completed: false,
})
state.newTodo = ''
}
})
const toggleTodo = $((id: number) => {
const todo = state.todos.find(t => t.id === id)
if (todo) {
todo.completed = !todo.completed
}
})
const filteredTodos = state.todos.filter(todo => {
if (state.filter === 'active') return !todo.completed
if (state.filter === 'completed') return todo.completed
return true
})
return (
<div class="todo-app">
<input
value={state.newTodo}
onInput$={(e) => state.newTodo = (e.target as HTMLInputElement).value}
onKeyDown$={(e) => e.key === 'Enter' && addTodo()}
placeholder="Add todo..."
/>
<button onClick$={addTodo}>Add</button>
<div class="filters">
{(['all', 'active', 'completed'] as const).map(filter => (
<button
key={filter}
class={{ active: state.filter === filter }}
onClick$={() => state.filter = filter}
>
{filter}
</button>
))}
</div>
<ul>
{filteredTodos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange$={() => toggleTodo(todo.id)}
/>
<span class={{ completed: todo.completed }}>{todo.text}</span>
</li>
))}
</ul>
</div>
)
})useComputed$ - computed values
import { component$, useSignal, useComputed$ } from '@builder.io/qwik'
export const ComputedExample = component$(() => {
const firstName = useSignal('John')
const lastName = useSignal('Doe')
const items = useSignal([10, 20, 30, 40, 50])
// Computed - automatycznie przeliczane przy zmianie dependencies
const fullName = useComputed$(() => {
return `${firstName.value} ${lastName.value}`
})
const total = useComputed$(() => {
return items.value.reduce((sum, item) => sum + item, 0)
})
const average = useComputed$(() => {
const sum = items.value.reduce((s, i) => s + i, 0)
return items.value.length > 0 ? sum / items.value.length : 0
})
return (
<div>
<p>Full Name: {fullName.value}</p>
<p>Total: {total.value}</p>
<p>Average: {average.value.toFixed(2)}</p>
</div>
)
})useTask$ - side effects
import { component$, useSignal, useTask$ } from '@builder.io/qwik'
export const TaskExample = component$(() => {
const searchQuery = useSignal('')
const results = useSignal<string[]>([])
const isLoading = useSignal(false)
// useTask$ - wykonuje się przy zmianie tracked signals
useTask$(async ({ track, cleanup }) => {
// track() rejestruje dependency
const query = track(() => searchQuery.value)
if (!query || query.length < 3) {
results.value = []
return
}
isLoading.value = true
// Debounce
const timeoutId = setTimeout(async () => {
try {
const response = await fetch(`/api/search?q=${query}`)
results.value = await response.json()
} finally {
isLoading.value = false
}
}, 300)
// cleanup - wywoływane przed następnym wykonaniem
cleanup(() => clearTimeout(timeoutId))
})
return (
<div>
<input
value={searchQuery.value}
onInput$={(e) => searchQuery.value = (e.target as HTMLInputElement).value}
placeholder="Search..."
/>
{isLoading.value && <p>Searching...</p>}
<ul>
{results.value.map((result, i) => (
<li key={i}>{result}</li>
))}
</ul>
</div>
)
})useVisibleTask$ - client-only effects
import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik'
export const ClientOnlyExample = component$(() => {
const windowWidth = useSignal(0)
const mousePosition = useSignal({ x: 0, y: 0 })
// useVisibleTask$ - wykonuje się TYLKO na kliencie
// Używaj gdy potrzebujesz dostępu do DOM/Browser APIs
useVisibleTask$(() => {
// Ten kod nigdy nie wykona się na serwerze
windowWidth.value = window.innerWidth
const handleResize = () => {
windowWidth.value = window.innerWidth
}
const handleMouseMove = (e: MouseEvent) => {
mousePosition.value = { x: e.clientX, y: e.clientY }
}
window.addEventListener('resize', handleResize)
window.addEventListener('mousemove', handleMouseMove)
// Cleanup
return () => {
window.removeEventListener('resize', handleResize)
window.removeEventListener('mousemove', handleMouseMove)
}
})
return (
<div>
<p>Window width: {windowWidth.value}px</p>
<p>Mouse: ({mousePosition.value.x}, {mousePosition.value.y})</p>
</div>
)
})Qwik City - Meta-framework
File-based Routing
src/routes/
├── index.tsx # → /
├── about/
│ └── index.tsx # → /about
├── blog/
│ ├── index.tsx # → /blog
│ └── [slug]/
│ └── index.tsx # → /blog/:slug
├── api/
│ └── users/
│ └── index.ts # → /api/users (server endpoint)
├── (auth)/ # Route group (nie dodaje do URL)
│ ├── login/
│ │ └── index.tsx # → /login
│ └── register/
│ └── index.tsx # → /register
└── layout.tsx # Shared layout dla wszystkich routesStrona z routeLoader$
// src/routes/blog/[slug]/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeLoader$, DocumentHead } from '@builder.io/qwik-city'
// routeLoader$ - data fetching na serwerze
export const usePost = routeLoader$(async ({ params, status }) => {
const response = await fetch(`https://api.example.com/posts/${params.slug}`)
if (!response.ok) {
status(404)
return null
}
return response.json() as Promise<{
title: string
content: string
author: string
date: string
}>
})
export default component$(() => {
// Dane są już załadowane na serwerze
const post = usePost()
if (!post.value) {
return <div>Post not found</div>
}
return (
<article class="blog-post">
<h1>{post.value.title}</h1>
<p class="meta">
By {post.value.author} on {new Date(post.value.date).toLocaleDateString()}
</p>
<div class="content" dangerouslySetInnerHTML={post.value.content} />
</article>
)
})
// Dynamic head/meta
export const head: DocumentHead = ({ resolveValue }) => {
const post = resolveValue(usePost)
return {
title: post?.title || 'Blog Post',
meta: [
{ name: 'description', content: post?.content?.slice(0, 160) || '' },
{ property: 'og:title', content: post?.title || 'Blog' },
],
}
}routeAction$ - Server Actions
// src/routes/contact/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city'
// Walidacja z Zod
const contactSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
message: z.string().min(10, 'Message must be at least 10 characters'),
})
// routeAction$ - server mutation
export const useContactForm = routeAction$(
async (data, { fail }) => {
try {
// Wyślij do API
const response = await fetch('https://api.example.com/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!response.ok) {
return fail(500, { message: 'Failed to send message' })
}
return { success: true, message: 'Message sent successfully!' }
} catch (error) {
return fail(500, { message: 'Server error' })
}
},
zod$(contactSchema)
)
export default component$(() => {
const action = useContactForm()
return (
<div class="contact-form">
<h1>Contact Us</h1>
{/* Form - progressive enhancement, działa bez JS */}
<Form action={action}>
<div class="field">
<label for="name">Name</label>
<input type="text" id="name" name="name" required />
{action.value?.fieldErrors?.name && (
<span class="error">{action.value.fieldErrors.name}</span>
)}
</div>
<div class="field">
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
{action.value?.fieldErrors?.email && (
<span class="error">{action.value.fieldErrors.email}</span>
)}
</div>
<div class="field">
<label for="message">Message</label>
<textarea id="message" name="message" rows={5} required />
{action.value?.fieldErrors?.message && (
<span class="error">{action.value.fieldErrors.message}</span>
)}
</div>
<button type="submit" disabled={action.isRunning}>
{action.isRunning ? 'Sending...' : 'Send Message'}
</button>
{action.value?.success && (
<p class="success">{action.value.message}</p>
)}
{action.value?.failed && (
<p class="error">{action.value.message}</p>
)}
</Form>
</div>
)
})server$ - Server Functions
import { component$, useSignal } from '@builder.io/qwik'
import { server$ } from '@builder.io/qwik-city'
// server$ - funkcja wykonywana na serwerze, wywoływana z klienta
const serverGreet = server$(async function (name: string) {
// Ten kod ZAWSZE działa na serwerze
// Masz dostęp do env, bazy danych, secrets
const apiKey = this.env.get('API_KEY')
console.log('Server log:', name, apiKey)
// Symulacja operacji serwerowej
await new Promise(resolve => setTimeout(resolve, 100))
return {
greeting: `Hello ${name} from server!`,
timestamp: new Date().toISOString(),
}
})
const fetchUserFromDB = server$(async function (userId: string) {
// Bezpieczne operacje na bazie danych
const db = await connectToDatabase()
const user = await db.users.findById(userId)
return user
})
export const ServerFunctionExample = component$(() => {
const result = useSignal<{ greeting: string; timestamp: string } | null>(null)
const isLoading = useSignal(false)
const callServer = $(async () => {
isLoading.value = true
try {
// Wywołanie server function z klienta
result.value = await serverGreet('World')
} finally {
isLoading.value = false
}
})
return (
<div>
<button onClick$={callServer} disabled={isLoading.value}>
{isLoading.value ? 'Loading...' : 'Call Server'}
</button>
{result.value && (
<div>
<p>{result.value.greeting}</p>
<small>At: {result.value.timestamp}</small>
</div>
)}
</div>
)
})Layout i nested routing
// src/routes/layout.tsx
import { component$, Slot } from '@builder.io/qwik'
import { routeLoader$ } from '@builder.io/qwik-city'
// Global data loader
export const useCurrentUser = routeLoader$(async ({ cookie }) => {
const token = cookie.get('auth-token')?.value
if (!token) return null
const response = await fetch('https://api.example.com/me', {
headers: { Authorization: `Bearer ${token}` },
})
if (!response.ok) return null
return response.json()
})
export default component$(() => {
const user = useCurrentUser()
return (
<div class="app">
<header>
<nav>
<a href="/">Home</a>
<a href="/blog">Blog</a>
<a href="/about">About</a>
{user.value ? (
<span>Welcome, {user.value.name}</span>
) : (
<a href="/login">Login</a>
)}
</nav>
</header>
<main>
{/* Slot renderuje child routes */}
<Slot />
</main>
<footer>
<p>© 2024 My App</p>
</footer>
</div>
)
})// src/routes/dashboard/layout.tsx - Nested layout
import { component$, Slot } from '@builder.io/qwik'
import { routeLoader$, useLocation } from '@builder.io/qwik-city'
// Middleware - redirect if not authenticated
export const onRequest: RequestHandler = async ({ redirect, cookie }) => {
const token = cookie.get('auth-token')?.value
if (!token) {
throw redirect(302, '/login')
}
}
export default component$(() => {
const location = useLocation()
const navItems = [
{ href: '/dashboard', label: 'Overview' },
{ href: '/dashboard/projects', label: 'Projects' },
{ href: '/dashboard/settings', label: 'Settings' },
]
return (
<div class="dashboard-layout">
<aside class="sidebar">
<nav>
{navItems.map(item => (
<a
key={item.href}
href={item.href}
class={{ active: location.url.pathname === item.href }}
>
{item.label}
</a>
))}
</nav>
</aside>
<div class="dashboard-content">
<Slot />
</div>
</div>
)
})API Routes
// src/routes/api/users/index.ts
import type { RequestHandler } from '@builder.io/qwik-city'
export const onGet: RequestHandler = async ({ json, query }) => {
const page = parseInt(query.get('page') || '1')
const limit = parseInt(query.get('limit') || '10')
const users = await db.users.findMany({
skip: (page - 1) * limit,
take: limit,
})
json(200, {
users,
pagination: { page, limit },
})
}
export const onPost: RequestHandler = async ({ json, parseBody, status }) => {
const body = await parseBody()
if (!body?.email || !body?.name) {
status(400)
return json(400, { error: 'Missing required fields' })
}
const user = await db.users.create({
data: { email: body.email, name: body.name },
})
json(201, user)
}// src/routes/api/users/[id]/index.ts
import type { RequestHandler } from '@builder.io/qwik-city'
export const onGet: RequestHandler = async ({ params, json, status }) => {
const user = await db.users.findById(params.id)
if (!user) {
status(404)
return json(404, { error: 'User not found' })
}
json(200, user)
}
export const onPut: RequestHandler = async ({ params, parseBody, json, status }) => {
const body = await parseBody()
const user = await db.users.update({
where: { id: params.id },
data: body,
})
if (!user) {
status(404)
return json(404, { error: 'User not found' })
}
json(200, user)
}
export const onDelete: RequestHandler = async ({ params, json, status }) => {
try {
await db.users.delete({ where: { id: params.id } })
json(200, { success: true })
} catch {
status(404)
json(404, { error: 'User not found' })
}
}Integracje
Tailwind CSS
npm run qwik add tailwind// Używanie Tailwind z Qwik
export const Button = component$<{
variant?: 'primary' | 'secondary'
}>((props) => {
const baseClasses = 'px-4 py-2 rounded-lg font-medium transition-colors'
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
}
return (
<button class={`${baseClasses} ${variants[props.variant || 'primary']}`}>
<Slot />
</button>
)
})Prisma
npm install prisma @prisma/client
npx prisma init// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client'
declare global {
var prisma: PrismaClient | undefined
}
export const prisma = globalThis.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') {
globalThis.prisma = prisma
}// src/routes/users/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeLoader$ } from '@builder.io/qwik-city'
import { prisma } from '~/lib/prisma'
export const useUsers = routeLoader$(async () => {
return prisma.user.findMany({
select: {
id: true,
name: true,
email: true,
createdAt: true,
},
orderBy: { createdAt: 'desc' },
take: 20,
})
})
export default component$(() => {
const users = useUsers()
return (
<div>
<h1>Users</h1>
<ul>
{users.value.map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
)
})Auth (z Lucia)
// src/lib/auth.ts
import { Lucia } from 'lucia'
import { PrismaAdapter } from '@lucia-auth/adapter-prisma'
import { prisma } from './prisma'
const adapter = new PrismaAdapter(prisma.session, prisma.user)
export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: process.env.NODE_ENV === 'production',
},
},
getUserAttributes: (attributes) => ({
email: attributes.email,
name: attributes.name,
}),
})// src/routes/login/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city'
import { lucia } from '~/lib/auth'
import { prisma } from '~/lib/prisma'
import { verifyPassword } from '~/lib/password'
export const useLogin = routeAction$(
async (data, { cookie, redirect, fail }) => {
const user = await prisma.user.findUnique({
where: { email: data.email },
})
if (!user || !await verifyPassword(data.password, user.passwordHash)) {
return fail(401, { message: 'Invalid credentials' })
}
const session = await lucia.createSession(user.id, {})
const sessionCookie = lucia.createSessionCookie(session.id)
cookie.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes)
throw redirect(302, '/dashboard')
},
zod$(z.object({
email: z.string().email(),
password: z.string().min(8),
}))
)
export default component$(() => {
const login = useLogin()
return (
<Form action={login}>
<input type="email" name="email" placeholder="Email" required />
<input type="password" name="password" placeholder="Password" required />
<button type="submit">Login</button>
{login.value?.failed && <p class="error">{login.value.message}</p>}
</Form>
)
})Zaawansowane wzorce
Context
import { component$, createContextId, useContextProvider, useContext, Slot } from '@builder.io/qwik'
// Definicja context
interface ThemeContext {
theme: 'light' | 'dark'
toggle: () => void
}
export const ThemeContextId = createContextId<ThemeContext>('theme')
// Provider
export const ThemeProvider = component$(() => {
const themeStore = useStore<ThemeContext>({
theme: 'light',
toggle: $(() => {
themeStore.theme = themeStore.theme === 'light' ? 'dark' : 'light'
}),
})
useContextProvider(ThemeContextId, themeStore)
return <Slot />
})
// Consumer
export const ThemeToggle = component$(() => {
const theme = useContext(ThemeContextId)
return (
<button onClick$={theme.toggle}>
Current: {theme.theme}
</button>
)
})Resource i Suspense
import { component$, useResource$, Resource } from '@builder.io/qwik'
export const AsyncDataExample = component$(() => {
const userId = useSignal('1')
// useResource$ - asynchroniczne dane z automatycznym SSR
const userResource = useResource$(async ({ track, cleanup }) => {
const id = track(() => userId.value)
const controller = new AbortController()
cleanup(() => controller.abort())
const response = await fetch(`/api/users/${id}`, {
signal: controller.signal,
})
return response.json()
})
return (
<div>
<select
value={userId.value}
onChange$={(e) => userId.value = (e.target as HTMLSelectElement).value}
>
<option value="1">User 1</option>
<option value="2">User 2</option>
<option value="3">User 3</option>
</select>
{/* Resource automatycznie obsługuje loading/error/success */}
<Resource
value={userResource}
onPending={() => <p>Loading user...</p>}
onRejected={(error) => <p>Error: {error.message}</p>}
onResolved={(user) => (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
)}
/>
</div>
)
})Deployment
Vercel
npm run qwik add vercel-edge
# lub
npm run qwik add vercel-serverlessCloudflare Pages
npm run qwik add cloudflare-pagesNode.js Server
npm run qwik add express
# lub
npm run qwik add fastifyStatic Site Generation
npm run qwik add static
npm run build.client
npm run build.server
npm run ssgPerformance porównanie
| Aplikacja | Qwik | Next.js | SvelteKit |
|---|---|---|---|
| E-commerce (50 produktów) | 3KB JS | 180KB JS | 45KB JS |
| Dashboard (10 widgets) | 2KB JS | 250KB JS | 60KB JS |
| Blog (10 postów) | 1.5KB JS | 120KB JS | 35KB JS |
| TTI (3G mobile) | 0.3s | 4.2s | 1.8s |
| LCP | 0.8s | 1.5s | 1.1s |
| CLS | 0 | 0.05 | 0.02 |
Cennik
- 100% darmowy - MIT License
- Builder.io Visual CMS: Opcjonalna integracja
- Free tier: 1 projekt
- Pro: $39/miesiąc
FAQ - Często zadawane pytania
Czy Qwik jest gotowy do produkcji?
Tak. Qwik jest w wersji stabilnej (v1.0+) i jest używany przez Builder.io oraz inne firmy w produkcji. Ma rosnącą społeczność i aktywny rozwój.
Jak Qwik się skaluje przy dużych aplikacjach?
Qwik skaluje się lepiej niż tradycyjne frameworki, ponieważ initial JS pozostaje stały (~1KB) niezależnie od rozmiaru aplikacji. JavaScript jest ładowany leniwie w miarę interakcji użytkownika.
Czy mogę używać bibliotek React z Qwik?
Qwik ma @builder.io/qwik-react który pozwala używać komponentów React w aplikacjach Qwik, ale tracisz wtedy korzyści resumability dla tych komponentów.
Jaka jest krzywa uczenia się Qwik?
Jeśli znasz React lub inne nowoczesne frameworki, Qwik jest łatwy do nauki. Składnia JSX jest znajoma. Główna różnica to zrozumienie koncepcji $ (lazy loading boundaries) i resumability.
Dlaczego symbol $ przy funkcjach?
$ oznacza granicę lazy-loadingu. Kompilator Qwik automatycznie wyodrębnia funkcje z $ do osobnych chunks, które są ładowane tylko gdy potrzebne. To klucz do osiągnięcia resumability.
Qwik - instant loading web applications
What is Qwik?
Qwik is a revolutionary JavaScript framework created by Misko Hevery (the creator of Angular) and the Builder.io team. It introduces a completely new approach to web application rendering through the concept of "resumability" - instead of traditional hydration, a Qwik application is interactive immediately after loading the HTML, without needing to download and execute all the JavaScript upfront.
The framework solves a fundamental problem of modern SPA applications: even with SSR, the user has to wait for the entire JS bundle to be downloaded and executed before the page becomes interactive. Qwik eliminates this problem by serializing the application state directly in the HTML and lazy-loading JavaScript at the level of individual event handlers.
Problem: hydration tax
How traditional SSR works
In traditional frameworks (React, Vue, Svelte), Server-Side Rendering works as follows:
1. Server → Renders HTML
2. Browser → Downloads HTML, displays a static page
3. Browser → Downloads the ENTIRE JavaScript bundle (50-500KB+)
4. Browser → Executes JavaScript
5. Framework → "Hydrates" the page (re-renders everything in memory)
6. Page → Becomes interactiveProblem: Steps 3-5 are the "hydration tax" - the time the user has to wait for interactivity. On slow mobile devices, this can take several seconds.
How Qwik resumability works
1. Server → Renders HTML + serializes state in attributes
2. Browser → Downloads HTML, displays the page
3. Page → Is IMMEDIATELY interactive
4. Browser → Downloads JS only for clicked elements (lazy)Benefit: No hydration = instant interactivity.
Qwik vs React/Vue - performance comparison
| Metric | Qwik | React | Vue | Angular |
|---|---|---|---|---|
| Initial JS | ~1KB | 50-100KB | 40-80KB | 80-150KB |
| Time to Interactive | Instant | 1-5s | 1-4s | 2-6s |
| Hydration | None (resume) | Yes | Yes | Yes |
| Lazy loading | Per-listener | Per-route | Per-route | Per-route |
| Bundle growth | Linear | Exponential | Exponential | Exponential |
| SSR overhead | Minimal | High | Medium | High |
Installation and configuration
Creating a new project
# Initialize a project with Qwik CLI
npm create qwik@latest
# Or with pnpm
pnpm create qwik@latest
# The interactive wizard will ask about:
# - Project name
# - Starter template (basic, with integrations)
# - Whether to add Qwik City (routing/meta-framework)Qwik City project structure
my-qwik-app/
├── src/
│ ├── components/ # Qwik components
│ │ ├── header/
│ │ │ └── header.tsx
│ │ └── footer/
│ │ └── footer.tsx
│ ├── routes/ # File-based routing
│ │ ├── index.tsx # / (home page)
│ │ ├── about/
│ │ │ └── index.tsx # /about
│ │ ├── blog/
│ │ │ ├── index.tsx # /blog
│ │ │ └── [slug]/
│ │ │ └── index.tsx # /blog/:slug
│ │ └── layout.tsx # Shared layout
│ ├── entry.ssr.tsx # SSR entry point
│ └── root.tsx # Root component
├── public/ # Static assets
├── vite.config.ts # Vite configuration
├── qwik.config.ts # Qwik configuration
└── package.jsonBasic configuration
// vite.config.ts
import { defineConfig } from 'vite'
import { qwikVite } from '@builder.io/qwik/optimizer'
import { qwikCity } from '@builder.io/qwik-city/vite'
export default defineConfig(() => {
return {
plugins: [qwikCity(), qwikVite()],
server: {
port: 5173,
},
preview: {
port: 4173,
},
}
})Qwik basics - syntax
Components with component$
// components/counter.tsx
import { component$, useSignal } from '@builder.io/qwik'
// The $ suffix means the function is lazy-loaded
export const Counter = component$(() => {
// useSignal - reactive state (fine-grained reactivity)
const count = useSignal(0)
return (
<div class="counter">
<p>Count: {count.value}</p>
{/* onClick$ - handler is lazy-loaded on first click */}
<button onClick$={() => count.value++}>
Increment
</button>
<button onClick$={() => count.value--}>
Decrement
</button>
</div>
)
})The meaning of the $ symbol (dollar sign)
The $ symbol in Qwik marks a lazy-loading boundary. Everything after $ is serialized and loaded only when needed:
import { component$, $, useSignal } from '@builder.io/qwik'
export const Example = component$(() => {
const message = useSignal('')
// $ creates a lazy-loaded function
const handleClick = $(() => {
// This code is in a separate chunk and loaded on click
message.value = 'Button clicked!'
console.log('This code was lazy-loaded')
})
// onClick$ automatically wraps in $
return (
<div>
<button onClick$={handleClick}>Click me</button>
<p>{message.value}</p>
</div>
)
})useSignal - reactive state
import { component$, useSignal } from '@builder.io/qwik'
export const SignalExample = component$(() => {
// Primitive values
const count = useSignal(0)
const name = useSignal('John')
const isActive = useSignal(true)
// Changing values
const increment = $(() => {
count.value++
})
const updateName = $((newName: string) => {
name.value = newName
})
return (
<div>
<p>Count: {count.value}</p>
<p>Name: {name.value}</p>
<p>Active: {isActive.value ? 'Yes' : 'No'}</p>
<button onClick$={increment}>+1</button>
<input
value={name.value}
onInput$={(e) => name.value = (e.target as HTMLInputElement).value}
/>
<button onClick$={() => isActive.value = !isActive.value}>
Toggle
</button>
</div>
)
})useStore - reactive objects
import { component$, useStore } from '@builder.io/qwik'
interface TodoItem {
id: number
text: string
completed: boolean
}
interface State {
todos: TodoItem[]
filter: 'all' | 'active' | 'completed'
newTodo: string
}
export const TodoApp = component$(() => {
// useStore for complex objects - deep reactivity
const state = useStore<State>({
todos: [],
filter: 'all',
newTodo: '',
})
const addTodo = $(() => {
if (state.newTodo.trim()) {
// Direct mutation - reactive!
state.todos.push({
id: Date.now(),
text: state.newTodo,
completed: false,
})
state.newTodo = ''
}
})
const toggleTodo = $((id: number) => {
const todo = state.todos.find(t => t.id === id)
if (todo) {
todo.completed = !todo.completed
}
})
const filteredTodos = state.todos.filter(todo => {
if (state.filter === 'active') return !todo.completed
if (state.filter === 'completed') return todo.completed
return true
})
return (
<div class="todo-app">
<input
value={state.newTodo}
onInput$={(e) => state.newTodo = (e.target as HTMLInputElement).value}
onKeyDown$={(e) => e.key === 'Enter' && addTodo()}
placeholder="Add todo..."
/>
<button onClick$={addTodo}>Add</button>
<div class="filters">
{(['all', 'active', 'completed'] as const).map(filter => (
<button
key={filter}
class={{ active: state.filter === filter }}
onClick$={() => state.filter = filter}
>
{filter}
</button>
))}
</div>
<ul>
{filteredTodos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange$={() => toggleTodo(todo.id)}
/>
<span class={{ completed: todo.completed }}>{todo.text}</span>
</li>
))}
</ul>
</div>
)
})useComputed$ - computed values
import { component$, useSignal, useComputed$ } from '@builder.io/qwik'
export const ComputedExample = component$(() => {
const firstName = useSignal('John')
const lastName = useSignal('Doe')
const items = useSignal([10, 20, 30, 40, 50])
// Computed - automatically recalculated when dependencies change
const fullName = useComputed$(() => {
return `${firstName.value} ${lastName.value}`
})
const total = useComputed$(() => {
return items.value.reduce((sum, item) => sum + item, 0)
})
const average = useComputed$(() => {
const sum = items.value.reduce((s, i) => s + i, 0)
return items.value.length > 0 ? sum / items.value.length : 0
})
return (
<div>
<p>Full Name: {fullName.value}</p>
<p>Total: {total.value}</p>
<p>Average: {average.value.toFixed(2)}</p>
</div>
)
})useTask$ - side effects
import { component$, useSignal, useTask$ } from '@builder.io/qwik'
export const TaskExample = component$(() => {
const searchQuery = useSignal('')
const results = useSignal<string[]>([])
const isLoading = useSignal(false)
// useTask$ - runs when tracked signals change
useTask$(async ({ track, cleanup }) => {
// track() registers a dependency
const query = track(() => searchQuery.value)
if (!query || query.length < 3) {
results.value = []
return
}
isLoading.value = true
// Debounce
const timeoutId = setTimeout(async () => {
try {
const response = await fetch(`/api/search?q=${query}`)
results.value = await response.json()
} finally {
isLoading.value = false
}
}, 300)
// cleanup - called before the next execution
cleanup(() => clearTimeout(timeoutId))
})
return (
<div>
<input
value={searchQuery.value}
onInput$={(e) => searchQuery.value = (e.target as HTMLInputElement).value}
placeholder="Search..."
/>
{isLoading.value && <p>Searching...</p>}
<ul>
{results.value.map((result, i) => (
<li key={i}>{result}</li>
))}
</ul>
</div>
)
})useVisibleTask$ - client-only effects
import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik'
export const ClientOnlyExample = component$(() => {
const windowWidth = useSignal(0)
const mousePosition = useSignal({ x: 0, y: 0 })
// useVisibleTask$ - runs ONLY on the client
// Use when you need access to DOM/Browser APIs
useVisibleTask$(() => {
// This code will never run on the server
windowWidth.value = window.innerWidth
const handleResize = () => {
windowWidth.value = window.innerWidth
}
const handleMouseMove = (e: MouseEvent) => {
mousePosition.value = { x: e.clientX, y: e.clientY }
}
window.addEventListener('resize', handleResize)
window.addEventListener('mousemove', handleMouseMove)
// Cleanup
return () => {
window.removeEventListener('resize', handleResize)
window.removeEventListener('mousemove', handleMouseMove)
}
})
return (
<div>
<p>Window width: {windowWidth.value}px</p>
<p>Mouse: ({mousePosition.value.x}, {mousePosition.value.y})</p>
</div>
)
})Qwik City - meta-framework
File-based routing
src/routes/
├── index.tsx # → /
├── about/
│ └── index.tsx # → /about
├── blog/
│ ├── index.tsx # → /blog
│ └── [slug]/
│ └── index.tsx # → /blog/:slug
├── api/
│ └── users/
│ └── index.ts # → /api/users (server endpoint)
├── (auth)/ # Route group (doesn't add to URL)
│ ├── login/
│ │ └── index.tsx # → /login
│ └── register/
│ └── index.tsx # → /register
└── layout.tsx # Shared layout for all routesPage with routeLoader$
// src/routes/blog/[slug]/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeLoader$, DocumentHead } from '@builder.io/qwik-city'
// routeLoader$ - server-side data fetching
export const usePost = routeLoader$(async ({ params, status }) => {
const response = await fetch(`https://api.example.com/posts/${params.slug}`)
if (!response.ok) {
status(404)
return null
}
return response.json() as Promise<{
title: string
content: string
author: string
date: string
}>
})
export default component$(() => {
// Data is already loaded on the server
const post = usePost()
if (!post.value) {
return <div>Post not found</div>
}
return (
<article class="blog-post">
<h1>{post.value.title}</h1>
<p class="meta">
By {post.value.author} on {new Date(post.value.date).toLocaleDateString()}
</p>
<div class="content" dangerouslySetInnerHTML={post.value.content} />
</article>
)
})
// Dynamic head/meta
export const head: DocumentHead = ({ resolveValue }) => {
const post = resolveValue(usePost)
return {
title: post?.title || 'Blog Post',
meta: [
{ name: 'description', content: post?.content?.slice(0, 160) || '' },
{ property: 'og:title', content: post?.title || 'Blog' },
],
}
}routeAction$ - server actions
// src/routes/contact/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city'
// Validation with Zod
const contactSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
message: z.string().min(10, 'Message must be at least 10 characters'),
})
// routeAction$ - server mutation
export const useContactForm = routeAction$(
async (data, { fail }) => {
try {
// Send to API
const response = await fetch('https://api.example.com/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!response.ok) {
return fail(500, { message: 'Failed to send message' })
}
return { success: true, message: 'Message sent successfully!' }
} catch (error) {
return fail(500, { message: 'Server error' })
}
},
zod$(contactSchema)
)
export default component$(() => {
const action = useContactForm()
return (
<div class="contact-form">
<h1>Contact Us</h1>
{/* Form - progressive enhancement, works without JS */}
<Form action={action}>
<div class="field">
<label for="name">Name</label>
<input type="text" id="name" name="name" required />
{action.value?.fieldErrors?.name && (
<span class="error">{action.value.fieldErrors.name}</span>
)}
</div>
<div class="field">
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
{action.value?.fieldErrors?.email && (
<span class="error">{action.value.fieldErrors.email}</span>
)}
</div>
<div class="field">
<label for="message">Message</label>
<textarea id="message" name="message" rows={5} required />
{action.value?.fieldErrors?.message && (
<span class="error">{action.value.fieldErrors.message}</span>
)}
</div>
<button type="submit" disabled={action.isRunning}>
{action.isRunning ? 'Sending...' : 'Send Message'}
</button>
{action.value?.success && (
<p class="success">{action.value.message}</p>
)}
{action.value?.failed && (
<p class="error">{action.value.message}</p>
)}
</Form>
</div>
)
})server$ - server functions
import { component$, useSignal } from '@builder.io/qwik'
import { server$ } from '@builder.io/qwik-city'
// server$ - function executed on the server, called from the client
const serverGreet = server$(async function (name: string) {
// This code ALWAYS runs on the server
// You have access to env, database, secrets
const apiKey = this.env.get('API_KEY')
console.log('Server log:', name, apiKey)
// Simulating a server operation
await new Promise(resolve => setTimeout(resolve, 100))
return {
greeting: `Hello ${name} from server!`,
timestamp: new Date().toISOString(),
}
})
const fetchUserFromDB = server$(async function (userId: string) {
// Safe database operations
const db = await connectToDatabase()
const user = await db.users.findById(userId)
return user
})
export const ServerFunctionExample = component$(() => {
const result = useSignal<{ greeting: string; timestamp: string } | null>(null)
const isLoading = useSignal(false)
const callServer = $(async () => {
isLoading.value = true
try {
// Calling a server function from the client
result.value = await serverGreet('World')
} finally {
isLoading.value = false
}
})
return (
<div>
<button onClick$={callServer} disabled={isLoading.value}>
{isLoading.value ? 'Loading...' : 'Call Server'}
</button>
{result.value && (
<div>
<p>{result.value.greeting}</p>
<small>At: {result.value.timestamp}</small>
</div>
)}
</div>
)
})Layout and nested routing
// src/routes/layout.tsx
import { component$, Slot } from '@builder.io/qwik'
import { routeLoader$ } from '@builder.io/qwik-city'
// Global data loader
export const useCurrentUser = routeLoader$(async ({ cookie }) => {
const token = cookie.get('auth-token')?.value
if (!token) return null
const response = await fetch('https://api.example.com/me', {
headers: { Authorization: `Bearer ${token}` },
})
if (!response.ok) return null
return response.json()
})
export default component$(() => {
const user = useCurrentUser()
return (
<div class="app">
<header>
<nav>
<a href="/">Home</a>
<a href="/blog">Blog</a>
<a href="/about">About</a>
{user.value ? (
<span>Welcome, {user.value.name}</span>
) : (
<a href="/login">Login</a>
)}
</nav>
</header>
<main>
{/* Slot renders child routes */}
<Slot />
</main>
<footer>
<p>© 2024 My App</p>
</footer>
</div>
)
})// src/routes/dashboard/layout.tsx - Nested layout
import { component$, Slot } from '@builder.io/qwik'
import { routeLoader$, useLocation } from '@builder.io/qwik-city'
// Middleware - redirect if not authenticated
export const onRequest: RequestHandler = async ({ redirect, cookie }) => {
const token = cookie.get('auth-token')?.value
if (!token) {
throw redirect(302, '/login')
}
}
export default component$(() => {
const location = useLocation()
const navItems = [
{ href: '/dashboard', label: 'Overview' },
{ href: '/dashboard/projects', label: 'Projects' },
{ href: '/dashboard/settings', label: 'Settings' },
]
return (
<div class="dashboard-layout">
<aside class="sidebar">
<nav>
{navItems.map(item => (
<a
key={item.href}
href={item.href}
class={{ active: location.url.pathname === item.href }}
>
{item.label}
</a>
))}
</nav>
</aside>
<div class="dashboard-content">
<Slot />
</div>
</div>
)
})API routes
// src/routes/api/users/index.ts
import type { RequestHandler } from '@builder.io/qwik-city'
export const onGet: RequestHandler = async ({ json, query }) => {
const page = parseInt(query.get('page') || '1')
const limit = parseInt(query.get('limit') || '10')
const users = await db.users.findMany({
skip: (page - 1) * limit,
take: limit,
})
json(200, {
users,
pagination: { page, limit },
})
}
export const onPost: RequestHandler = async ({ json, parseBody, status }) => {
const body = await parseBody()
if (!body?.email || !body?.name) {
status(400)
return json(400, { error: 'Missing required fields' })
}
const user = await db.users.create({
data: { email: body.email, name: body.name },
})
json(201, user)
}// src/routes/api/users/[id]/index.ts
import type { RequestHandler } from '@builder.io/qwik-city'
export const onGet: RequestHandler = async ({ params, json, status }) => {
const user = await db.users.findById(params.id)
if (!user) {
status(404)
return json(404, { error: 'User not found' })
}
json(200, user)
}
export const onPut: RequestHandler = async ({ params, parseBody, json, status }) => {
const body = await parseBody()
const user = await db.users.update({
where: { id: params.id },
data: body,
})
if (!user) {
status(404)
return json(404, { error: 'User not found' })
}
json(200, user)
}
export const onDelete: RequestHandler = async ({ params, json, status }) => {
try {
await db.users.delete({ where: { id: params.id } })
json(200, { success: true })
} catch {
status(404)
json(404, { error: 'User not found' })
}
}Integrations
Tailwind CSS
npm run qwik add tailwind// Using Tailwind with Qwik
export const Button = component$<{
variant?: 'primary' | 'secondary'
}>((props) => {
const baseClasses = 'px-4 py-2 rounded-lg font-medium transition-colors'
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
}
return (
<button class={`${baseClasses} ${variants[props.variant || 'primary']}`}>
<Slot />
</button>
)
})Prisma
npm install prisma @prisma/client
npx prisma init// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client'
declare global {
var prisma: PrismaClient | undefined
}
export const prisma = globalThis.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') {
globalThis.prisma = prisma
}// src/routes/users/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeLoader$ } from '@builder.io/qwik-city'
import { prisma } from '~/lib/prisma'
export const useUsers = routeLoader$(async () => {
return prisma.user.findMany({
select: {
id: true,
name: true,
email: true,
createdAt: true,
},
orderBy: { createdAt: 'desc' },
take: 20,
})
})
export default component$(() => {
const users = useUsers()
return (
<div>
<h1>Users</h1>
<ul>
{users.value.map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
)
})Auth (with Lucia)
// src/lib/auth.ts
import { Lucia } from 'lucia'
import { PrismaAdapter } from '@lucia-auth/adapter-prisma'
import { prisma } from './prisma'
const adapter = new PrismaAdapter(prisma.session, prisma.user)
export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: process.env.NODE_ENV === 'production',
},
},
getUserAttributes: (attributes) => ({
email: attributes.email,
name: attributes.name,
}),
})// src/routes/login/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city'
import { lucia } from '~/lib/auth'
import { prisma } from '~/lib/prisma'
import { verifyPassword } from '~/lib/password'
export const useLogin = routeAction$(
async (data, { cookie, redirect, fail }) => {
const user = await prisma.user.findUnique({
where: { email: data.email },
})
if (!user || !await verifyPassword(data.password, user.passwordHash)) {
return fail(401, { message: 'Invalid credentials' })
}
const session = await lucia.createSession(user.id, {})
const sessionCookie = lucia.createSessionCookie(session.id)
cookie.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes)
throw redirect(302, '/dashboard')
},
zod$(z.object({
email: z.string().email(),
password: z.string().min(8),
}))
)
export default component$(() => {
const login = useLogin()
return (
<Form action={login}>
<input type="email" name="email" placeholder="Email" required />
<input type="password" name="password" placeholder="Password" required />
<button type="submit">Login</button>
{login.value?.failed && <p class="error">{login.value.message}</p>}
</Form>
)
})Advanced patterns
Context
import { component$, createContextId, useContextProvider, useContext, Slot } from '@builder.io/qwik'
// Context definition
interface ThemeContext {
theme: 'light' | 'dark'
toggle: () => void
}
export const ThemeContextId = createContextId<ThemeContext>('theme')
// Provider
export const ThemeProvider = component$(() => {
const themeStore = useStore<ThemeContext>({
theme: 'light',
toggle: $(() => {
themeStore.theme = themeStore.theme === 'light' ? 'dark' : 'light'
}),
})
useContextProvider(ThemeContextId, themeStore)
return <Slot />
})
// Consumer
export const ThemeToggle = component$(() => {
const theme = useContext(ThemeContextId)
return (
<button onClick$={theme.toggle}>
Current: {theme.theme}
</button>
)
})Resource and Suspense
import { component$, useResource$, Resource } from '@builder.io/qwik'
export const AsyncDataExample = component$(() => {
const userId = useSignal('1')
// useResource$ - async data with automatic SSR
const userResource = useResource$(async ({ track, cleanup }) => {
const id = track(() => userId.value)
const controller = new AbortController()
cleanup(() => controller.abort())
const response = await fetch(`/api/users/${id}`, {
signal: controller.signal,
})
return response.json()
})
return (
<div>
<select
value={userId.value}
onChange$={(e) => userId.value = (e.target as HTMLSelectElement).value}
>
<option value="1">User 1</option>
<option value="2">User 2</option>
<option value="3">User 3</option>
</select>
{/* Resource automatically handles loading/error/success */}
<Resource
value={userResource}
onPending={() => <p>Loading user...</p>}
onRejected={(error) => <p>Error: {error.message}</p>}
onResolved={(user) => (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
)}
/>
</div>
)
})Deployment
Vercel
npm run qwik add vercel-edge
# or
npm run qwik add vercel-serverlessCloudflare Pages
npm run qwik add cloudflare-pagesNode.js server
npm run qwik add express
# or
npm run qwik add fastifyStatic Site Generation
npm run qwik add static
npm run build.client
npm run build.server
npm run ssgPerformance comparison
| Application | Qwik | Next.js | SvelteKit |
|---|---|---|---|
| E-commerce (50 products) | 3KB JS | 180KB JS | 45KB JS |
| Dashboard (10 widgets) | 2KB JS | 250KB JS | 60KB JS |
| Blog (10 posts) | 1.5KB JS | 120KB JS | 35KB JS |
| TTI (3G mobile) | 0.3s | 4.2s | 1.8s |
| LCP | 0.8s | 1.5s | 1.1s |
| CLS | 0 | 0.05 | 0.02 |
Pricing
- 100% free - MIT License
- Builder.io Visual CMS: Optional integration
- Free tier: 1 project
- Pro: $39/month
FAQ - frequently asked questions
Is Qwik production-ready?
Yes. Qwik is in a stable version (v1.0+) and is used by Builder.io and other companies in production. It has a growing community and active development.
How does Qwik scale with large applications?
Qwik scales better than traditional frameworks because the initial JS remains constant (~1KB) regardless of application size. JavaScript is lazily loaded as the user interacts with the page.
Can I use React libraries with Qwik?
Qwik has @builder.io/qwik-react which allows you to use React components in Qwik applications, but you lose the resumability benefits for those components.
What is the learning curve for Qwik?
If you know React or other modern frameworks, Qwik is easy to learn. The JSX syntax is familiar. The main difference is understanding the $ concept (lazy loading boundaries) and resumability.
Why the $ symbol on functions?
$ marks a lazy-loading boundary. The Qwik compiler automatically extracts functions with $ into separate chunks that are loaded only when needed. This is the key to achieving resumability.