Guide
Qwik fundamentals explained
A typical React e-commerce page ships 200–400 KB of JavaScript before
the “Add to cart” button works. The framework must download, parse,
and execute the entire component tree — even widgets the user never clicks
— because hydration replays the server render on the client.
Qwik takes a different path: resumability.
The server embeds enough serialized state in HTML that the browser can attach
event listeners to specific DOM nodes and fetch only the code for the interaction
the user triggers. Built on
Vite with JSX
syntax familiar from
React,
Qwik City adds file-based routing, route loaders, and
server$ functions for type-safe server calls. This guide explains
resumability vs hydration, $ signals, the optimizer’s lazy
boundaries, Qwik City patterns, static and edge deployment, a Harbor Commerce
product catalog worked example, a framework decision table, common pitfalls, and
a production checklist — complementing our
Astro islands guide
and
rendering modes guide
without repeating their content-first focus.
Resumability vs hydration
Traditional SSR frameworks render HTML on the server, then ship a JavaScript bundle that reconstructs component state and attaches listeners across the whole tree. That hydration pass is often the largest contributor to Total Blocking Time on content-plus-interaction sites.
Qwik inverts the cost model. During SSR, the framework serializes:
- Component boundaries — which lazy chunks correspond to which DOM subtrees.
- Signal state — reactive values as JSON the client can resume without re-running setup logic.
- Event handler references — QRL (Qwik Runtime Language) symbols that map clicks and inputs to dynamically imported functions.
On first interaction — say, opening a cart drawer — the browser fetches a small chunk containing only that handler and its dependencies. No global hydration sweep runs. The result is near-zero JavaScript on initial load with interactivity that scales with user behavior, not page complexity.
How this differs from Astro islands
Astro defaults to static HTML and hydrates explicit islands. Qwik targets
applications where most of the page may eventually become interactive
(filters, carts, dashboards) but you still refuse to pay upfront for unused
code. Both optimize
Core Web Vitals;
Qwik keeps a unified component model rather than mixing .astro
shells with separate framework islands.
Components, $ signals, and reactivity
Qwik components are functions that return JSX. Reactivity uses
signals — reactive primitives created with
useSignal() or the shorthand useSignal(0) exported
as count with a .value accessor. In templates, the
optimizer rewrites bare references like {count.value} to subscribe
automatically.
useSignal(initial)— mutable reactive cell; updates trigger fine-grained DOM patches.useStore({ ... })— reactive object graph for form state and nested records.useComputed$()— derived values that recompute when dependencies change.useTask$()— runs on the server during SSR and on the client when tracked signals change; use for side effects tied to state.useVisibleTask$()— runs only in the browser after the component becomes visible; ideal for analytics and observers.useResource$()— async data fetching with loading/error states, similar to a lightweight loader inside a component.
Event handlers must be declared with the $ suffix —
onClick$={() => count.value++} — so the optimizer can
extract them into lazy-loadable QRL modules. Forgetting $ on
handlers or tasks is the most common beginner mistake; the compiler will warn,
but inconsistent usage breaks lazy boundaries.
Props and children
Components receive props as a plain object argument. Slots use
<Slot /> for composition. Because components can run on
server and client, keep props JSON-serializable — same constraint as
other SSR frameworks.
Qwik City: routing, loaders, and actions
Qwik City is the official meta-framework (comparable to
Next.js App Router or SvelteKit). Routes live under src/routes/
using directory conventions:
index.tsx— route component for that path segment.layout.tsx— shared wrapper with<Slot />for nested routes.[slug]/index.tsx— dynamic segments.api/*/index.ts— HTTP endpoints returningResponseobjects.
Route loaders
routeLoader$() fetches data on the server (or at build time for
static prerender) and exposes it to the route component via
loader.value. Loaders run before render, keeping data fetching
out of client bundles — similar to Remix loaders or Next.js server
components, but with Qwik’s lazy resume model on the client.
Form actions
routeAction$() handles POST submissions with progressive
enhancement: forms work without JavaScript, and Qwik enhances them when
scripts load. Validation schemas (often Zod) can live alongside actions for
shared server/client typing.
server$ functions
server$() wraps arbitrary async functions so they execute only
on the server when called from client event handlers — a type-safe RPC
layer without exposing API routes manually. Use for mutations, privileged
database queries, and third-party API keys that must never ship to browsers.
The optimizer and lazy loading
Qwik’s optimizer (a Vite plugin) is non-negotiable.
It scans source for $-suffixed functions and splits each into
a separate lazy chunk referenced by a unique QRL hash in the HTML. Without
the optimizer, Qwik loses resumability and behaves like a conventional SSR
framework.
- Granularity — individual click handlers, effects, and components can load independently.
- Prefetch —
usePrefetchand linkprefetchhints load likely-next chunks during idle time. - Preloader — Qwik City can emit module preload hints for critical QRLs on high-traffic routes.
- Bundle analysis — inspect
dist/chunk sizes after build; oversized shared chunks usually mean a missing$boundary on a large utility imported by many handlers.
Third-party libraries that assume immediate DOM APIs may need
useVisibleTask$ wrappers or dynamic import() inside
$ callbacks. Charting, maps, and rich text editors are common
candidates for lazy, visible-only loading.
Static, SSR, and edge deployment
Qwik City supports multiple output targets via adapters:
- Static site generation (SSG) — prerender routes at build; ideal for marketing pages and catalogs with periodic rebuilds.
- SSR on Node — per-request rendering for personalized or authenticated views.
- Edge adapters — Cloudflare Workers, Netlify Edge, and Vercel Edge run Qwik close to users with low cold-start latency.
Choose SSG when product data changes on a schedule (nightly catalog sync); choose SSR when every request needs session cookies or A/B assignment. Hybrid patterns prerender public catalog pages while SSR-ing account dashboards. Pair deployment choice with our CDN guide for cache rules on static HTML and immutable QRL chunks.
Worked example: Harbor Commerce product catalog
Harbor Commerce sells industrial fasteners across twelve regional warehouses. The legacy React SPA catalog scored 42 on mobile Performance: 520 KB JavaScript before filters responded. Requirements: sub-1.5s LCP on 4G, faceted search, live stock badges, and cart persistence without sacrificing SEO on twelve thousand SKU pages.
- Route structure —
/products/[sku]/index.tsxwithrouteLoader$fetching SKU metadata and inventory from PostgreSQL; prerender top 2,000 SKUs, SSR long-tail on demand. - Faceted filters — filter state in
useStore; each filter chip’sonClick$loads a 3 KB chunk updating URL query params for shareable links. - Stock badge —
server$function polls warehouse API on “Check availability” click instead of polling on every page load. - Cart drawer — cart icon uses
useVisibleTask$to hydrate drawer only after first scroll; initial HTML includes empty cart shell with zero JS. - Deploy — Cloudflare Workers adapter; HTML cached 60s at edge, QRL chunks cached immutable; nightly static rebuild for sitemap and top SKUs.
Outcome: mobile Performance rose to 91; JavaScript on first load dropped to 18 KB (core Qwik runtime only). Median time-to-interactive for “Add to cart” fell from 4.2s to 0.6s because the handler chunk prefetched on hover. SEO crawlers received full HTML from loaders without executing client bundles.
Framework decision table
| Your situation | Favor Qwik when | Consider alternatives |
|---|---|---|
| E-commerce or marketing site with heavy interactivity | Many interactive widgets but users touch only a few per visit | Astro if 90% of pages are read-only prose |
| Team knows React JSX | Signals and $ handlers map cleanly to existing mental models |
Next.js if ecosystem packages and hiring pool dominate |
| Core Web Vitals are a hard SLA | Resumability minimizes TBT on mid-tier mobile devices | Static HTML + HTMX for mostly form-driven apps |
| Real-time dashboard with constant updates | Less ideal — frequent signal churn loads many chunks | React/Vue SPA optimized for sustained client state |
| Enterprise Angular codebase | Greenfield perf-sensitive frontends, not migration of NgModules | Angular if DI, RxJS, and corporate standards are fixed |
| Edge-first global latency | Official Workers/Netlify adapters with small cold starts | Remix or Hono + HTMX for simpler server-rendered flows |
Common pitfalls
- Missing
$on handlers and tasks — breaks lazy extraction; bundles balloon and resumability fails silently until you inspect chunk sizes. - Non-serializable loader data — passing class instances or functions from
routeLoader$to the client throws at runtime; return plain JSON. - Importing heavy libraries at module top level — pulls code into shared chunks; dynamic import inside
$callbacks instead. - Treating Qwik like a hydration framework — eagerly loading all QRLs on mount defeats the architecture; trust lazy loading and prefetch hints.
- useTask$ for browser-only APIs — runs on server during SSR and crashes on
window; useuseVisibleTask$or checkisServer. - Skipping optimizer in custom Vite configs — without
qwikVite()andqwikCity()plugins, builds look fine but lose resume metadata. - Over-fetching in loaders for static pages — personalize only routes that need SSR; prerender catalog shells when data is batch-updated.
Production checklist
- Scaffold with official starter (
npm create qwik@latest) so optimizer and City plugins are prewired. - Audit every event handler and task for
$suffix; enable ESLint Qwik rules in CI. - Define
routeLoader$for data-heavy routes; keep loaders free of secrets in returned payloads. - Wrap privileged operations in
server$; never embed API keys in client bundles. - Measure Lighthouse TBT and JS bytes on a cold cache before and after each feature.
- Configure prefetch on primary navigation links for likely-next routes.
- Choose SSG vs SSR per route; document which paths prerender in
vite.config. - Set long-cache headers on QRL chunks at CDN; HTML cache TTL shorter if prices or auth vary.
- Run E2E tests that click interactive paths to ensure lazy chunks load in production builds.
- Generate sitemap from prerendered routes; verify crawlers receive loader-populated HTML.
Key takeaways
- Resumability attaches interactivity from serialized HTML without hydrating the full component tree — JavaScript scales with user actions, not page size.
- $ signals provide fine-grained reactivity;
$-suffixed handlers and tasks enable the optimizer to split lazy chunks. - Qwik City adds file routing,
routeLoader$, form actions, andserver$for full-stack patterns on Vite. - Performance wins are largest on interaction-heavy pages where most widgets stay idle — e-commerce, docs with search, configurators.
- Qwik complements content-first Astro and ecosystem-heavy Next.js rather than replacing them outright.
Related reading
- Vite fundamentals explained — the build tool and dev server Qwik uses
- SSR, CSR, SSG, and ISR explained — rendering vocabulary for Qwik City output modes
- Core Web Vitals explained — LCP, INP, and CLS metrics Qwik targets
- React fundamentals explained — JSX and component patterns Qwik developers already know