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 returning Response objects.

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.
  • PrefetchusePrefetch and link prefetch hints 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.

  1. Route structure/products/[sku]/index.tsx with routeLoader$ fetching SKU metadata and inventory from PostgreSQL; prerender top 2,000 SKUs, SSR long-tail on demand.
  2. Faceted filters — filter state in useStore; each filter chip’s onClick$ loads a 3 KB chunk updating URL query params for shareable links.
  3. Stock badgeserver$ function polls warehouse API on “Check availability” click instead of polling on every page load.
  4. Cart drawer — cart icon uses useVisibleTask$ to hydrate drawer only after first scroll; initial HTML includes empty cart shell with zero JS.
  5. 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; use useVisibleTask$ or check isServer.
  • Skipping optimizer in custom Vite configs — without qwikVite() and qwikCity() 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, and server$ 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