Tutorial · Intermediate

Qwik Best Practices for Production Apps in 2026: 25+ Patterns That Actually Move Your Lighthouse Score

A comprehensive guide to qwik best practices: expert recommendations. Follow these steps to get started.

23 min read·Intermediate·8 steps·Updated 2026

Qwik's performance story is real, but only if you write code the optimizer can split. Most teams that ship a slow Qwik app made the same five mistakes: reading signals in the component function, registering events with useVisibleTask$, leaking window.location into the resumable layer, never useComputed$-ing derived state, and shipping a single root component that ruins lazy boundaries. This guide enumerates 25+ best practices across eight categories (performance, state, events, routing, code organization, testing, accessibility/security, and deployment) with the exact code patterns to copy and the anti-patterns to avoid. Every rule is grounded in the official Qwik 1.x docs and Builder.io performance benchmarks, with the version-specific guidance current as of Qwik 1.13 (May 2026) and the Vite 6 toolchain.

The single rule that subsumes the rest: every JavaScript byte you avoid shipping eagerly is a Lighthouse point earned. Qwik's resumability gives you the mechanism. These practices make sure you actually use it.


The 10 Qwik Anti-Patterns to Avoid (Bookmark This)

Print this and pin it next to your monitor. Every item maps to a specific section below.

#Anti-patternWhy it kills performanceFix
1Reading signal.value directly inside component$(() => {...})The whole template re-runs on every signal changeWrap derivations in useComputed$ or move the read into JSX
2Registering DOM events via useVisibleTask$Forces eager core download regardless of whether the event firesUse useOn / useOnDocument / useOnWindow
3Accessing window.location in component bodies or useVisibleTask$Forces client-side execution that should be SSRUse the useLocation() hook from @builder.io/qwik-city
4One giant root componentOptimizer has nothing to split on; whole app downloads as one chunkSplit into many component$ boundaries; each one is a lazy chunk
5Putting non-serializable values (functions, class instances, promises) on useStoreResumability breaks at the boundary; full re-render on the clientKeep state JSON-serializable; use noSerialize() for client-only refs
6Arrow functions for tasks and visible tasksOptimizer cannot consistently name and split the symbolUse named functions inside $() for tasks: useTask$(function trackSearch({ track }) {...})
7Importing server-only secrets in client-reachable modulesBuilder bundles secrets into client chunksUse .server.ts suffix or routeLoader$ server-only execution
8Calling useVisibleTask$ with side effects that could run server-sideCosts hydration-class JS for work the server could do at build timeMove to useTask$ (runs in SSR), routeLoader$, or build-time helpers
9Custom useState-style abstractions wrapping useSignalAdds friction for the optimizer and prevents fine-grained reactivityUse useSignal and useStore directly; resist React-ish wrappers
10No Lighthouse gate in CIPerformance regresses silently as bundles growAdd a Lighthouse-as-CI step pinned to thresholds (LCP < 2.5s, CLS < 0.1, INP < 200ms)

Performance: Optimize for the Optimizer

Qwik's runtime is small because the optimizer can lift, split, and lazy-load almost anything you write, provided you let it. The four practices below are the ones the official docs lead with, restated here with the additional context developers need to apply them in production code.

1. Inline derivations in templates

A derived value computed outside the JSX (e.g. const isBig = signal.value > 0 ? 'big' : 'small') forces the entire component function to re-execute whenever the signal changes. Move the expression into the JSX and the optimizer can scope the re-render to the specific JSX node.

tsx
// Anti-pattern
export const Counter = component$(() => {
  const count = useSignal(0);
  const label = count.value > 0 ? 'positive' : 'non-positive';
  return <div>{label} {count.value}</div>;
});

// Best practice
export const Counter = component$(() => {
  const count = useSignal(0);
  return (
    <div>
      {count.value > 0 ? 'positive' : 'non-positive'} {count.value}
    </div>
  );
});

The difference is invisible during development but ships measurably less JS for the re-render path.

2. Read signals inside useTask$ or useComputed$, not in the component body

Qwik tracks every read of signal.value. Reads outside JSX inside the component function attach the surrounding function to the signal's dependency graph; on change, the function re-runs. Move the read into a useComputed$ and only that function re-runs, returning a new signal whose value the JSX then reads.

tsx
// Anti-pattern
export const Cart = component$(() => {
  const items = useSignal<Item[]>([]);
  const total = items.value.reduce((a, b) => a + b.price, 0); // re-runs the whole component
  return <div>Total: ${total}</div>;
});

// Best practice
export const Cart = component$(() => {
  const items = useSignal<Item[]>([]);
  const total = useComputed$(() => items.value.reduce((a, b) => a + b.price, 0));
  return <div>Total: ${total.value}</div>;
});

For state that depends on async work (network calls, file reads), reach for useResource$ instead of useComputed$ so loading and error states are first-class.

3. Use useVisibleTask$ only as the last resort

useVisibleTask$ triggers eager JavaScript download to wire up the callback, defeating the resumability story for that boundary. The decision tree from the official docs, expanded:

  • Need to listen for an event? → useOn / useOnDocument / useOnWindow
  • Need to react to state changes that can run on the server? → useTask$
  • Need to react to state changes that must run on the client only? → useTask$ guarded with isBrowser
  • Genuinely need to run code only when the component scrolls into view? → useVisibleTask$({ strategy: 'document-idle' }) and document the trade-off in a code comment

When useVisibleTask$ truly is the only fit, suppress the linter warning explicitly so future readers see the deliberate choice: // eslint-disable-next-line qwik/no-use-visible-task.

4. Delay framework "core" execution where you can

Qwik's runtime (called "core") is itself a chunk. You can sometimes defer it past first paint by structuring event registration so the handler doesn't reference component-scope variables. When the handler closes only over module-scope or external imports, the optimizer can register the listener without loading core.

tsx
import { libId } from 'library';
const GLOBAL_ID = 'global-id';

export const Component = component$(() => {
  const ref = useSignal();          // component-scope
  const id = useId();               // component-scope

  // Executes core at load time — closes over component-scope `ref`
  useOnDocument('qidle', $(() => console.log(ref)));

  // Does NOT execute core at load time — closes only over module-scope
  useOnDocument('qidle', $(() => console.log(GLOBAL_ID)));
  useOnDocument('qidle', $(() => console.log(libId)));
  useOnDocument('qidle', $(() => console.log('static literal')));

  return <p ref={ref}></p>;
});

This is a power-user move. Use it on the highest-traffic routes; skip it on dashboards.

5. Keep components small

Every component$ boundary is a potential lazy-load chunk. A 600-line "page" component download-and-renders as one chunk; the same page split into a header, a hero, a feature grid, a CTA strip, and a footer downloads as five chunks the optimizer can load in priority order. Aim for components under 150 lines of body code; route-level components should orchestrate, not render markup directly.


Signals & State Management

Qwik state lives across the resumability boundary, which means the rules are tighter than React's. The first thing that breaks for new Qwik developers is the assumption that any value is fair game in useStore, and it isn't.

6. Pick the right primitive

  • useSignal(value): a single value, primitive or object reference. Use it for the 80% case.
  • useStore({ ... }): a keyed object you want fine-grained reactivity across multiple keys. The store is a proxy; reads of store.someKey track only that key.
  • useComputed$(() => derived): synchronous derived value from other signals/stores.
  • useResource$(({ track }) => fetchSomething()): async derived value with built-in loading/error states. Use this anywhere you would useEffect(() => fetch(...)) in React.

7. Keep state JSON-serializable

Qwik serializes state across the SSR → client boundary. Functions, class instances, DOM references, promises, and circular structures break serialization. The result is either a runtime error or a silent fallback to full re-render on the client, and the second is worse because nothing surfaces in dev.

When you must hold a non-serializable value (a third-party SDK instance, a chart library handle), wrap it with noSerialize(). The state slot becomes undefined after SSR resume, so you re-create the value lazily on the client.

tsx
import { noSerialize, type NoSerialize } from '@builder.io/qwik';

interface State { player: NoSerialize<HLSPlayer> | undefined }

export const Player = component$(() => { const state = useStore<State>({ player: undefined });

useOnDocument('qvisible', $(() => { const HLS = require('hls.js').default; state.player = noSerialize(new HLS()); }));

return <video ref={(el) => state.player?.attachMedia(el)} />; });

text

### 8. Use named functions inside `$()`, not arrow functions

The optimizer uses function names to build the symbol manifest. Anonymous arrow functions get serial-numbered names that are stable inside a build but change as soon as you add or remove a sibling. Named functions produce stable, grep-able symbols.

```tsx
// Anti-pattern (arrow function inside $)
useTask$(({ track }) => { track(searchSig); doWork(searchSig.value); });

// Best practice (named function inside $)
useTask$(function onSearchChange({ track }) {
  track(searchSig);
  doWork(searchSig.value);
});

This also pays off when reading Lighthouse waterfall traces. Symbol names show up in the chunk filename.

9. Name signal variables with a Sig suffix when scope is ambiguous

In components with many local variables, distinguishing reactive from non-reactive values speeds code review. Adopt the qwikifiers style guide convention: signals end in Sig (countSig, userSig, cartSig), stores end in Store. The pattern is convention, not enforced by the compiler, but it scales.

10. Avoid React-ish abstractions over signals

The temptation to wrap useSignal in a useState-shaped hook to ease the React → Qwik migration is real and almost always wrong. The wrapper hides the value/setter symmetry that the optimizer relies on for fine-grained tracking. Pay the small migration cost once and use the primitives directly.


Events & Lifecycle

Almost every "Qwik feels slow" report on the GitHub discussions traces back to a useVisibleTask$ that should have been useOn. The choice tree below is the one to internalize.

11. The event-registration choice tree

You want to react to…UseWhy
A user gesture on this component's rootuseOn('click', $(...))Listener attached lazily; core loads only when the event fires
A user gesture on documentuseOnDocument('mousemove', $(...))Same; works for events that bubble or for tracking
A user gesture on windowuseOnWindow('resize', $(...))Same; ideal for resize / scroll patterns
A signal change that can run on the serveruseTask$(({track}) => ...)Runs in SSR + on the client; cheapest
A signal change that must run on the client onlyuseTask$ + isBrowser guardMarginally more expensive than pure server-side
The component scrolling into view, with no other optionuseVisibleTask$Eager JS; last resort

12. Replace useVisibleTask$ event registration verbatim

This is the single largest perf win for most legacy Qwik apps. The verbatim refactor:

tsx
// Anti-pattern
useVisibleTask$(({ cleanup }) => {
  const handler = (e: MouseEvent) => console.log(e.x, e.y);
  document.addEventListener('mousemove', handler);
  cleanup(() => document.removeEventListener('mousemove', handler));
});

// Best practice
useOnDocument('mousemove', $((e: MouseEvent) => {
  console.log(e.x, e.y);
}));

The optimizer wires the listener at the right boundary; cleanup is automatic.

13. Use qidle and qvisible strategies deliberately

For one-shot client-side work, useOn('qvisible', ...) runs when the component becomes visible; useOn('qidle', ...) runs on the browser's idle callback. Both are cheaper than useVisibleTask$ because they only trigger once per boundary. Prefer qidle for non-critical telemetry and qvisible for setup that needs the element in view (lazy image hydration, intersection-observer init).

14. Don't fight Qwik's "no useEffect" mental model

Qwik does not have useEffect. Stop looking for one. The work useEffect(() => fetch()) does in React belongs to routeLoader$ (server-side at request time) or useResource$ (anywhere, with first-class loading/error). The work useEffect(() => subscribe()) does belongs to useTask$ with track. The work useEffect(() => { /* run once on mount */ }) does belongs to useOn('qvisible', ...). Reaching for useVisibleTask$ is almost never the right answer.


Routing, Server Functions & Code Organization

Qwik City is opinionated about file layout, and the opinions are correct. Fight them and you ship slower apps.

15. Use file-based routing exclusively

A route is src/routes/<path>/index.tsx. Layouts are src/routes/<path>/layout.tsx. Loaders are routeLoader$() exported from the route file. Resist the urge to build a custom router on top of useLocation() and useNavigate(); the file-based router is what powers prefetching and resumability handoff.

16. Prefer routeLoader$ over client fetches for initial data

routeLoader$ runs on the server during the route request, so the first paint already contains the data. The client never needs to wait for the network on initial render.

tsx
// src/routes/products/[id]/index.tsx
import { routeLoader$ } from '@builder.io/qwik-city';

export const useProduct = routeLoader$(async ({ params, env }) => {
  const res = await fetch(`${env.get('API')}/products/${params.id}`, {
    headers: { Authorization: env.get('API_TOKEN')! },
  });
  return res.json() as Promise<Product>;
});

export default component$(() => {
  const product = useProduct();
  return <h1>{product.value.name}</h1>;
});

The env.get('API_TOKEN') runs server-side only; the token never ships to the client.

17. Use routeAction$ for form mutations; never POST from useTask$

Form submissions belong in routeAction$. The action runs on the server, returns typed errors, and integrates with progressive-enhancement form behavior. The form submits without JavaScript at all.

tsx
import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city';

export const useSignupAction = routeAction$(
  async (data, { redirect }) => {
    await createUser(data);
    throw redirect(302, '/welcome');
  },
  zod$({ email: z.string().email(), password: z.string().min(8) })
);

export default component$(() => {
  const action = useSignupAction();
  return (
    <Form action={action}>
      <input name="email" type="email" />
      <input name="password" type="password" />
      <button>Sign up</button>
      {action.value?.failed && <p>{action.value.fieldErrors?.email}</p>}
    </Form>
  );
});

The zod$ validator runs server-side; no client validation library is needed for the security-critical check.

18. Segregate server-only code with the .server.ts suffix

Any module ending .server.ts (or .server.tsx) is excluded from client bundles by the Qwik plugin. Use it for database clients, secret-holding helpers, and any logic that must never reach the browser. The compiler will fail the build if a .server.ts module is imported from a non-server context, a fail-fast guard against accidental leaks.

19. Co-locate route-private components under _components/

Files and folders prefixed with _ are ignored by Qwik City's route resolver but reachable as siblings. Put route-only components in src/routes/products/_components/ProductCard.tsx, leaving src/components/ for the genuinely shared library. This avoids the false-shared-library problem where every "shared" component is used by exactly one route.

20. Skip barrel files

index.ts re-export files (export * from './foo'; export * from './bar') defeat tree-shaking in many bundler configurations and confuse the Qwik optimizer's symbol attribution. Import directly from the leaf module: import { ProductCard } from './ProductCard', not import { ProductCard } from './'.


Migrating from React or Next.js: The Mental Model Shift

Most Qwik teams arrive from React, and the same handful of muscle-memory patterns produce the same handful of bugs. The fastest way through the migration is to internalize the API translation table below, then run the four-step playbook on one route at a time rather than the whole app at once.

20a. The React-to-Qwik API translation table

React / Next.jsQwik equivalentNotes
useState(initial)useSignal(initial)Same idea, different mutation surface: sig.value = next instead of setSig(next)
useEffect(() => fetch(...)) (data fetch on mount)routeLoader$(async () => fetch(...))Runs server-side at request time; data is in the first paint
useEffect(() => sub.on(); return () => sub.off())useTask$(({ cleanup, track }) => { ... cleanup(() => off()) })Track dependencies explicitly with track(signal)
useEffect(() => { runOnce() }, []) (mount-only side effect)useOn('qvisible', $(runOnce)) or useOn('qidle', $(runOnce))Defers JS download until the lifecycle event fires
useMemo(() => derive(a, b), [a, b])useComputed$(() => derive(a.value, b.value))Auto-tracked; no dependency array to maintain
useContext (provider + consumer)createContextId + useContextProvider + useContextSame shape, Qwik names
useReduceruseStore({state, dispatch$})Plain JS for the reducer body inside the store
useRef(domEl)useSignal<Element>() passed to ref={sig} on the JSX nodeSignals double as refs; one less primitive to learn
useRef(mutableValue)useStore({ current: value }) with a single current keySame escape valve
Custom hook useFoo()A $-suffixed helper returning the signals/computedsThe wrapper itself is not a "hook" — there is no hook-rule discipline
<Link href="/x"> (Next.js)<Link href="/x"> from @builder.io/qwik-citySame API; resumability handles the prefetch
getServerSideProps (Next.js)routeLoader$One serialization step instead of two
getStaticProps (Next.js)routeLoader$ with cache.control header set, or static adapterSame loader, different deploy adapter
API route at pages/api/foo.tsrouteAction$ co-located with the form, or server$() for a callable RPCCo-located is usually correct
next.config.js rewritesQwik City file-based routes; no rewrite layer neededThe file path is the route

The two patterns that most often confuse React migrators: there is no useEffect, and signals mutate by assignment, not setter. Stop hunting for either.

20b. The four-step migration playbook (per route)

  1. File structure first. Move the route page to src/routes/<path>/index.tsx and any layout to src/routes/<path>/layout.tsx. Resist co-importing the React component as-is; the route file should call new Qwik components, not adapt old ones.
  2. Loaders next. Translate every useEffect(() => fetch(...)) and every Next.js getServerSideProps into a routeLoader$. The first paint should already contain the data.
  3. Components third. Translate components leaf-up: lowest leaf first, then composite, then layout. Use the translation table above. Resist wrapping signals in custom hooks during the migration; you can refactor for shared logic later.
  4. Fine-grained reactivity audit last. Once the route renders, profile with the production build (npm run build && npm run preview) and look at the network panel. Symbols that download you did not expect indicate a useVisibleTask$ or a component-scope signal read that should be a useComputed$. Fix those last because they are the highest-leverage perf wins.

20c. Common React-import pitfalls

The first time a React migrator imports useState from React inside a Qwik component, the build does not fail until runtime, when resumability cannot serialize the React internal state. Set an ESLint rule that bans import { useState, useEffect, useMemo, useRef } from 'react' in your Qwik project root, with an exception for any genuinely-React component you are still rendering inside a qwik-react interop boundary. The build failure happens in CI rather than at the user's first paint.


Testing, Accessibility & Security

These three categories appear together because they share a discipline: catch the failure in code review, not in production. None of the top-3 ranking sources for "qwik best practices" covers any of them; this section closes that gap.

21. Test components with Vitest + @builder.io/qwik/testing

Component tests in Qwik use the official testing renderer. Avoid Jest. The Qwik plugin set targets Vite/Vitest.

ts
// counter.spec.ts
import { createDOM } from '@builder.io/qwik/testing';
import { Counter } from './counter';

test('increments on click', async () => {
  const { screen, render, userEvent } = await createDOM();
  await render(<Counter />);
  expect(screen.outerHTML).toContain('count: 0');
  await userEvent('button', 'click');
  expect(screen.outerHTML).toContain('count: 1');
});

For e2e and visual regression, use Playwright against a vite build && vite preview server. That mode is closer to production than vite dev.

22. Mock server functions, not HTTP

When testing a route component that uses routeLoader$, mock the loader directly via useProduct.use(() => mockValue) inside the test setup. Mocking fetch at the network layer works but couples the test to the loader implementation.

23. Practice semantic JSX

Use the right HTML element for the job: <button> for actions, <a> for navigation, <form> for mutations. Avoid <div onClick$={...}> outside genuinely non-interactive surfaces. Qwik's resumability boundary is JS-light by default, so semantic HTML means the page is usable before any JavaScript reaches the client, which is the whole point.

24. Manage focus across the resumability boundary

Resumability means the browser sees real HTML before JS arrives. If you depend on JS to set initial focus (a modal opening, a search box auto-focusing), the user sees a flash of focus-less UI. Use the autofocus attribute for the first-paint focus state, then progressively enhance with JS for subsequent state changes.

25. Keep all secrets server-side

Never use import.meta.env.PUBLIC_* for anything sensitive. PUBLIC_* variables are inlined into client bundles. Use the env.get('KEY') API inside routeLoader$/routeAction$/server$ functions only, and store the key without the PUBLIC_ prefix.

ts
// .env
DATABASE_URL=postgres://...
PUBLIC_SITE_URL=https://example.com  // safe to expose

// Anti-pattern (leaks DB URL into client bundle)
const db = createClient(import.meta.env.DATABASE_URL);

// Best practice (server-only)
export const useUsers = routeLoader$(async ({ env }) => {
  const db = createClient(env.get('DATABASE_URL')!);
  return db.query('SELECT id, name FROM users');
});

26. Validate every server-function input with Zod

routeAction$(action, zod$(schema)) is the canonical pattern; server$ functions should validate their inputs explicitly at the top of the body. Treat server functions like API endpoints, because that is what they are.

27. Set a Content-Security-Policy header

In src/routes/layout.tsx, set a strict CSP via the response headers:

tsx
export const onGet: RequestHandler = ({ headers }) => {
  headers.set(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;"
  );
};

Qwik does inject inline event handlers for resumability, which is why 'unsafe-inline' is required for script-src today; the team is tracking a nonce-based replacement on the 2.0 roadmap.


Deployment, Observability & Real Benchmarks

A Qwik app that ships with the wrong adapter ships without resumability. Observe production, gate Lighthouse in CI, and you keep the wins.

28. Pick the adapter that matches your runtime

RuntimeAdapterNotes
Vercel (Node)@builder.io/qwik-city/adapters/vercel-edge/vite (edge) or vercel/vite (Node)Edge is faster cold-start; Node has wider Node-API compatibility
Cloudflare Pagescloudflare-pages/viteLimit: 1 MB compressed worker; trim deps
Netlifynetlify-edge/vite or netlify-functions/viteEdge for the hot routes; Functions for the heavy ones
Node self-hostnode-server/viteRun behind a reverse proxy; gzip + brotli at the proxy
Static exportstatic/vitePure SSG; no server functions; works on any CDN

Add the adapter with npm run qwik add <adapter>; the CLI updates vite.config.ts and adds the deploy scripts.

29. Set Lighthouse thresholds in CI

A regression that drops LCP from 1.2 s to 3.0 s should fail the build, not surface from a Search Console alert two weeks later. Use @lhci/cli against the preview server:

yaml
# .github/workflows/lighthouse.yml
- run: npm run build && npm run preview &
- run: npx wait-on http://localhost:4173
- run: npx -y @lhci/cli@latest autorun \
    --collect.url=http://localhost:4173 \
    --assert.preset=lighthouse:recommended \
    --assert.assertions.largest-contentful-paint='["error", { "maxNumericValue": 2500 }]' \
    --assert.assertions.cumulative-layout-shift='["error", { "maxNumericValue": 0.1 }]' \
    --assert.assertions.interaction-to-next-paint='["error", { "maxNumericValue": 200 }]'

The thresholds map to the Core Web Vitals pass bands: LCP ≤ 2.5 s, CLS ≤ 0.1, INP ≤ 200 ms.

30. Use Qwik Insights for the production telemetry the docs don't ship

Qwik Insights (@builder.io/qwik-labs package, the <Insights> component in <head>) is the resumability-aware RUM tool. It surfaces which symbols actually downloaded on the client per session, which gives you the real "what is my app shipping" picture that Lighthouse synthesizes but doesn't sample. Install it on the highest-traffic routes first.

tsx
// src/root.tsx
import { component$ } from '@builder.io/qwik';
import { Insights } from '@builder.io/qwik-labs';
import { QwikCityProvider, RouterOutlet } from '@builder.io/qwik-city';

export default component$(() => {
  return (
    <QwikCityProvider>
      <head>
        <Insights publicApiKey={import.meta.env.PUBLIC_QWIK_INSIGHTS_KEY} />
      </head>
      <body>
        <RouterOutlet />
      </body>
    </QwikCityProvider>
  );
});

Insights captures the symbol manifest per session, so you can see in the dashboard which symbols are being downloaded most often, which ones never download (candidates for deletion), and which symbols are correlated with longer interactions. The metric most teams care about: median total JS shipped per route by user. If that number creeps up release-over-release, that is the signal to run the audit in practice 32. The dashboard is free for projects under 100K monthly events; above that the team-plan pricing matches typical RUM tooling.

Treat the Insights data as ground truth for your perf story. Lighthouse on a synthetic device is a useful CI gate; Insights on real sessions is what actually pays the perf bill in front of users.

31. Wire Sentry for runtime errors

The @sentry/qwik integration (community SDK) attaches to both client and server boundaries. The minimum useful integration is in src/root.tsx:

tsx
import { init } from '@sentry/qwik';

init({ dsn: import.meta.env.PUBLIC_SENTRY_DSN, tracesSampleRate: 0.1, });

text

Server-function errors propagate to Sentry without further setup once the route handler is wrapped.

### 32. Benchmarks: what to expect, what to measure

The Builder.io team has published benchmark comparisons showing Qwik counter examples reaching Lighthouse mobile scores of 100/100/100/100 with sub-1 s LCP, against React equivalents in the 60–80 range with 2.5+ s LCP. Those numbers are for a trivial counter; your production app will be lower. The discipline is to measure your own routes:

| Metric | Bad | OK | Good | Target |
|---|---|---|---|---|
| Lighthouse mobile (perf) | < 50 | 50–80 | 80–95 | ≥ 95 |
| LCP (mobile, 4G) | > 4.0 s | 2.5–4.0 s | 1.5–2.5 s | ≤ 1.5 s |
| INP | > 500 ms | 200–500 ms | 100–200 ms | ≤ 100 ms |
| CLS | > 0.25 | 0.1–0.25 | 0.05–0.1 | ≤ 0.05 |
| Total JS shipped on initial route | > 200 KB | 80–200 KB | 30–80 KB | ≤ 30 KB |

The "Total JS shipped" row is the Qwik-specific one. Most React Next.js apps land at 150–400 KB on initial route; a well-built Qwik app lands at 1–20 KB because resumability defers everything until needed. If your Qwik app is above 80 KB on initial route, audit for the `useVisibleTask$` anti-patterns above.

---

## Frequently Asked Questions

**Is Qwik production-ready?**

Yes. Qwik 1.0 shipped in May 2023 and is on its 1.13 release as of May 2026. Builder.io, the framework's commercial backer, runs Qwik in production. Multiple SaaS companies report shipping on Qwik 1.x stable since late 2023. The 2.0 roadmap is additive: no breaking-change wholesale rewrite is planned.

**Is Qwik faster than React or Next.js?**

For initial-page-load and interactivity-after-load, yes, measurably. Qwik ships fundamentally less JavaScript on first render because of resumability. For long-lived single-page interactions where hydration cost is amortized, the gap narrows. The right comparison is your specific app on real devices: build the same screen in both, run Lighthouse on a 4G profile, look at LCP and the total JS shipped to the client.

**How do I reduce my Qwik bundle size?**

Start with the five performance practices above. If those are clean, audit your `useVisibleTask$` usage (anti-pattern #2 above). Every one you can replace with `useOn` removes a chunk from the initial payload. Then split large components into smaller `component$` boundaries (anti-pattern #4). Then run `npm run build && npm run preview` and inspect the network panel for the slug names. Symbols named after `useTask$` calls indicate code that loaded which you didn't expect.

**When should I use `useVisibleTask$`?**

When you must run code on the client only, must run it exactly once when an element becomes visible, and cannot wire it via `useOn('qvisible')` because you need access to component-scope refs or signals that aren't available to the lazier mechanism. In every other case, use `useTask$` (with `isBrowser` guard if needed), `useOn`, `useOnDocument`, or `useOnWindow`.

**Do I need a server to run Qwik?**

No. The static adapter (`@builder.io/qwik-city/adapters/static/vite`) produces a pure SSG output that runs on any CDN. You give up `routeLoader$` and `routeAction$` (no server runtime) but keep resumability, fine-grained reactivity, and the entire client-side feature set. Many marketing sites and documentation portals on Qwik use the static adapter.

**How does Qwik compare to Astro?**

Astro and Qwik solve overlapping problems differently. Astro defaults to zero JavaScript and adds it back per-island via explicit hydration directives like `client:load`, `client:visible`, `client:idle`. Qwik defaults to full app capability and removes JavaScript via resumability, which means an entire route can be interactive with effectively no client-side JS shipped. For mostly-static content sites (marketing, blogs, documentation), Astro is usually simpler. For interactive applications where the whole page is dynamic but you still want first-paint speed, Qwik is the better fit. The two frameworks are increasingly chosen for adjacent rather than identical use cases.

**What is the Qwik 2.0 timeline?**

The Qwik core team has publicly discussed Qwik 2.0 as additive rather than disruptive. Expected improvements include a nonce-based CSP that removes the `'unsafe-inline'` requirement, lighter core after the WASM-backed parser ships, better TypeScript inference for `routeLoader$` returns, and refined error boundaries. A specific release date has not been pinned as of May 2026; track the official `qwik.dev` announcements and the Builder.io blog for the public roadmap. Greenfield 1.x apps written today will migrate without rewrites.

**How do I handle authentication in Qwik?**

Use `routeLoader$` to read the session cookie server-side and either return the user object (for authenticated routes) or throw `redirect(302, '/login')` (for protected routes). For form-based login, use `routeAction$` with a Zod schema for credentials, write the session cookie via `cookie.set('sid', token, { httpOnly: true, secure: true, sameSite: 'Strict' })`, and redirect on success. The community SDKs for Auth.js, Clerk, and Supabase Auth all have working Qwik adapters as of Qwik 1.13.

---

## More Reading

- Considering alternatives to Qwik? See our [Qwik alternatives guide](/launch-school/alternatives/qwik) for an honest comparison against Astro, Solid, Svelte, and Next.js.
- New to Qwik? Start with [Getting Started with Qwik](/launch-school/tutorials/qwik-getting-started) (covers `npm create qwik@latest`, the basic counter, and the first route).
- Building a different framework? Read our matching best-practices guides for [Astro](/launch-school/tutorials/astro-best-practices), [Remix](/launch-school/tutorials/remix-best-practices), and [Cargo](/launch-school/tutorials/cargo-best-practices).

External authority references: [Qwik official docs](https://qwik.dev/docs/guides/best-practices/) and the [Builder.io Qwik launch announcement](https://www.builder.io/blog/introducing-qwik-framework) (the post that introduced resumability publicly).

## What you'll learn

## Prerequisites

## Step by step

## Common mistakes

Qwik Best Practices: Expert Recommendations FAQ

Common questions about this tutorial

Yes. Qwik 1.0 shipped in May 2023 and is on its 1.13 release as of May 2026. Builder.io, the framework's commercial backer, runs Qwik in production. Multiple SaaS companies have shipped on Qwik 1.x stable since late 2023, and the 2.0 roadmap is additive rather than a breaking-change rewrite.
For initial-page-load and interactivity-after-load, yes, measurably. Qwik ships fundamentally less JavaScript on first render because of resumability. For long-lived single-page interactions where hydration cost is amortized, the gap narrows. The right comparison is your specific app on real devices: build the same screen in both, run Lighthouse on a 4G profile, and compare LCP and total JS shipped to the client.
Start with the five performance practices in this guide. If those are clean, audit useVisibleTask$ usage; every one you can replace with useOn removes a chunk from the initial payload. Then split large components into smaller component$ boundaries. Then run npm run build && npm run preview and inspect the network panel for symbol names to find unexpected eager loads.
When you must run code on the client only, must run it exactly once when an element becomes visible, and cannot wire it via useOn('qvisible') because you need access to component-scope refs or signals that aren't available to the lazier mechanism. In every other case, use useTask$ (with isBrowser guard if needed), useOn, useOnDocument, or useOnWindow.
No. The static adapter produces a pure SSG output that runs on any CDN. You give up routeLoader$ and routeAction$ but keep resumability, fine-grained reactivity, and the entire client-side feature set. Many marketing sites and documentation portals on Qwik use the static adapter.
Astro defaults to zero JavaScript and adds it back per-island via explicit hydration directives. Qwik defaults to full app capability and removes JavaScript via resumability. For mostly-static content sites, Astro is usually simpler. For interactive applications where the whole page is dynamic but you still want first-paint speed, Qwik is the better fit.
The Qwik core team has publicly described Qwik 2.0 as additive rather than disruptive. Expected improvements include a nonce-based CSP, lighter core after the WASM-backed parser ships, better TypeScript inference for routeLoader$ returns, and refined error boundaries. No specific release date has been pinned as of May 2026; track the official qwik.dev announcements and the Builder.io blog for updates.
Use routeLoader$ to read the session cookie server-side and either return the user object or throw redirect(302, '/login'). For form-based login, use routeAction$ with a Zod schema for credentials, write the session cookie via cookie.set('sid', token, { httpOnly: true, secure: true, sameSite: 'Strict' }), and redirect on success. Auth.js, Clerk, and Supabase Auth all have working Qwik adapters as of Qwik 1.13.