Skip to content

Top 31 Qwik Interview Questions and Answers [2026]

November 9, 2026 31 min read
Qwik interview guide by Second Talent
TL;DR: Qwik interviews in 2026 test whether a candidate can explain resumability, predict what crosses a $ boundary, and keep JavaScript off the page until a user acts. The current stable release is Qwik 1.20.3 (October 25, 2026, packages @builder.io/qwik and @builder.io/qwik-city). Qwik v2 is a release candidate, 2.0.0-rc.4 from the same day, shipped as @qwik.dev/core and @qwik.dev/router, and senior candidates should know what it changes.

On October 1, 2026 the Qwik team published the first Qwik 2.0 release candidate, a ground-up rewrite that renames Qwik City to Qwik Router, requires Vite 8 with Rolldown, and lets useComputed$ take async functions.

The latest builds are Qwik 1.20.3 for production and @qwik.dev/core 2.0.0-rc.4 for teams testing v2. The questions below are for senior front-end and full-stack interviews on Qwik and Qwik City, with five find-the-bug rounds.

Key takeaways
  1. 1Qwik v2 is still a release candidate: 2.0.0-rc.4 (October 25, 2026). Stable production apps run 1.20.3.
  2. 2@qwik.dev/core declares a peer dependency on Vite 8, while Qwik 1.20.3 accepts Vite 5 through 7.
  3. 3In v2, route loaders become async signals: value.failed is gone and reading .value on a failed loader throws the error.
  4. 4A server$ call skips layout and page middleware, so an auth check in layout.tsx does not protect it.

Resumability and the Optimizer

1. What is resumability, and how does it differ from hydration?

Resumability means the browser continues from where the server stopped, without re-running components to rebuild state and attach listeners.

A hydrating framework sends HTML, then downloads the component code and executes it again on the client so it can find the event handlers and recreate state.

Qwik serializes the application state, the component boundaries and the listener locations into the HTML, so the client has nothing to replay at startup.

The resumability docs frame the result as startup cost that does not grow with app size: the page becomes interactive with a small loader script, and component code downloads only when an interaction or a state change needs it.

A strong candidate adds the trade-off. The first click on a cold page may wait for a network fetch of that handler's chunk, which is why prefetching and preloading matter (question 5), and the serialized state makes the HTML larger.

A weak answer calls it "lazy hydration" or "partial hydration". Islands still hydrate each island; Qwik does not run the component on the client at all until its state changes and it has to re-render.

2. What does the $ suffix mean, and what does the Optimizer do with it?

A trailing $ marks a lazy-loading boundary.

The Optimizer, a build step in the Qwik Vite plugin, finds each function passed to a $ API, moves it into its own module (a segment, sometimes called a symbol), and replaces it with a QRL: a serializable reference that names the chunk and export to import later.

Per the dollar sign docs, any API ending in $ follows this contract, including component$, onClick$, useTask$, routeLoader$ and server$.

import { component$, useSignal } from '@builder.io/qwik';

export const Counter = component$(() => {
  const count = useSignal(0);
  return (
    <button onClick$={() => count.value++}>
      Clicked {count.value} times
    </button>
  );
});

Here the Optimizer produces at least two segments: the component render function and the click handler.

The server renders HTML whose button carries an attribute pointing at the handler's chunk, and the handler's code is fetched only when someone clicks.

Candidates should know you can create your own boundary with $() and that the Qrl forms (componentQrl, useTaskQrl) are what the $ forms compile to.

3. What can a function inside a $ boundary capture?

Three kinds of references are allowed: top-level exported symbols, which the extracted module can import; const variables from the enclosing scope whose values are serializable; and values Qwik already knows how to serialize, such as signals and stores.

The serialization guide states the rule: only serializable data can cross a $ boundary, and a non-top-level variable must be declared with const.

The trap is that some violations pass static analysis and fail at runtime. The guide says capturing a non-serializable value "will fail at runtime because Qwik does not know how to serialize it."

Instances of custom classes are the common case. The ESLint plugin's qwik/valid-lexical-scope rule flags many of these at edit time, and a candidate who has shipped Qwik will mention running it in CI.

A strong answer explains why the rule exists. The extracted function will run later, possibly in another environment, with only the serialized snapshot of its captured variables.

A let would need its later reassignments tracked, and a class instance would need its prototype and private state rebuilt, which the serializer cannot do.

4. How does Qwik serialize state, and what does noSerialize do?

During SSR, Qwik walks the reachable state of signals, stores, captured variables and QRLs and writes it into the page so the client can rebuild those objects on demand. In Qwik 1.x that state sits in a <script type="qwik/json"> block.

In v2, per the v2 migration guide, it moves into qwik/vnode and qwik/state script blocks, which matters for any tooling that parsed the old tag.

The serialization guide lists promises as serializable, so a pending value can be sent and resolved on the client.

noSerialize(value) marks a value to be left out. The value works during the current execution, and after the page resumes on the client it reads as undefined.

Use it for third-party objects like a map widget, an editor instance or a WebSocket, and re-create them on the client when needed.

import { component$, useSignal, useStore, noSerialize, type NoSerialize } from '@builder.io/qwik';

export const MapView = component$(() => {
  const el = useSignal<HTMLDivElement>();
  const state = useStore<{ map: NoSerialize<MapLib> }>({ map: undefined });

  return (
    <div
      ref={el}
      onClick$={async () => {
        if (!state.map) {
          const { createMap } = await import('./map-lib');
          state.map = noSerialize(createMap(el.value!));
        }
      }}
    />
  );
});

A strong candidate also watches the size of serialized state. Loading a 2 MB API response into a store puts that payload in the HTML, so the fix is to select the fields the page renders before they reach a signal.

5. What is qwikloader, and how does a click become a code download?

Qwikloader is a small inline or module script that registers global listeners for the event types the page uses.

On each event, it walks from the target up the DOM looking for an element with a matching handler attribute, reads the QRL from it, imports that chunk and calls the exported handler with the event and the element.

In 1.x these attributes look like on:click; in v2 they are q-e:click, q-d:click and q-w:click for element, document and window listeners. JSX is unchanged, so application code does not see the difference.

The cost of a cold network fetch on first interaction is handled by preloading.

Qwik 1.14.0 added automatic bundle preloading based on the bundle graph the build emits, and later 1.x releases tuned it, for example 1.19.0 stopped merging useVisibleTask$ code into other segments to avoid preloading too much.

A candidate should be able to explain how the build knows which chunks are likely next and how to inspect the bundle graph when a page preloads more than it should.

6. What does this handler do, and why is e.preventDefault() a problem?

<a
  href="/checkout"
  onClick$={(e) => {
    e.preventDefault();
    openCartDrawer();
  }}
>
  Cart
</a>

On a resumed page, the handler code may not be loaded when the click happens. Qwikloader has to fetch the chunk, and by the time the handler runs the browser has already acted on the event, so the link navigates to /checkout.

Calls to preventDefault() and stopPropagation() only work when they run synchronously inside the dispatch.

Qwik gives two fixes. The declarative one is the preventdefault:click attribute on the element, which qwikloader honors before loading anything.

The programmatic one is sync$(), which inlines a small serialized function into the HTML so it runs during dispatch; pass an array of handlers when you also need the lazy part.

<a
  href="/checkout"
  preventdefault:click
  onClick$={() => openCartDrawer()}
>
  Cart
</a>

<input
  onKeyDown$={[
    sync$((e: KeyboardEvent) => {
      if (e.key === 'Enter') e.preventDefault();
    }),
    $(() => submitSearch()),
  ]}
/>

A strong candidate adds that sync$ bodies cannot capture variables, because they are inlined as source text, and keeps them short.

Signals, Stores and Tasks

7. When do you use useSignal, and when useStore?

Use useSignal for a single value, including primitives, and useStore for an object whose properties should each be reactive. A signal tracks reads and writes of .value.

A store is a proxy that tracks each property, and it is deep by default, so nested objects and arrays are reactive too; useStore(initial, { deep: false }) limits tracking to the top level.

The trap is mutation of objects held in a signal. user.value.name = 'Ana' changes the object without writing .value, so nothing re-renders (question 20). Replace the object (user.value = { ...user.value, name: 'Ana' }) or use a store.

A good candidate also explains that stores add proxy overhead and deep tracking of large data, so a list of 5,000 rows that only gets replaced whole fits better in a signal.

8. How does Qwik update the DOM when a signal changes?

Qwik records which component templates, text nodes, attributes and tasks read each signal, and updates only those subscribers.

If JSX reads {count.value}, the component that read it is scheduled to re-render, and its render function chunk is downloaded if it is not loaded yet.

If JSX receives the signal itself, {count}, Qwik binds the signal to that text node and updates it without running the component.

The interview signal is whether a candidate knows that a component's code is downloaded only when it has to re-render.

A parent that passes a signal down to a child without reading it does not need its own render code on the client.

Strong candidates design state so the components that change are small leaves, and they can explain why reading a large store at the top of a page pulls the whole page's render code onto the client after the first update.

9. Compare useComputed$, useTask$ and useResource$ in Qwik 1.x.

  • useComputed$ derives a read-only signal from other state. It re-runs when a tracked input changes and, since 1.18.0, notifies listeners only when its value changes.
  • useTask$ runs side effects. It runs once where the component first renders (server or browser), before rendering and blocking it, and re-runs in the browser when values passed to track() change.
  • useResource$ runs async work that re-runs on tracked changes and exposes a resource you render with <Resource value={...} onPending onResolved onRejected />. It does not block rendering.

The tasks docs recommend useComputed$ when you only derive state synchronously. A task that writes one signal from another is a common code smell, because it runs an extra pass and can leave an intermediate value on screen.

For v2, the answer changes: async useComputed$ replaces useResource$, which is deprecated and scheduled for removal in v3 (question 30). Qwik 1.20.1 removed the deprecation warning on async useComputed$ in 1.x for the same reason.

10. What does this print, and where?

export const Clicker = component$(() => {
  const count = useSignal(0);

  useTask$(() => {
    console.log('count is', count.value);
  });

  return <button onClick$={() => count.value++}>{count.value}</button>;
});

If the page is server-rendered and the user clicks three times, "count is 0" prints once, in the server's console, and nothing prints in the browser.

The task has no track() call, so it runs once where the component first renders, and that is the server. Reading count.value inside a task does not subscribe the task the way it would in a template.

useTask$(({ track }) => {
  const value = track(() => count.value);
  console.log('count is', value);
});

With track, the task re-runs in the browser after each click.

Strong candidates also mention isServer and isBrowser from @builder.io/qwik/build for code that must run in one environment, and they know that the first run during SSR blocks the response until the task's promise resolves.

11. When is useVisibleTask$ the right tool, and what does it cost?

Use it only for code that needs the live DOM or a browser API at startup and cannot wait for an event: measuring layout, starting a third-party widget that must be visible, or subscribing to an observer.

It runs in the browser after render and does not block it.

The default strategy runs the task when the component enters the viewport via an intersection observer; { strategy: 'document-ready' } runs it as soon as the app loads, and 'document-idle' waits for idle time.

The docs call it a last resort because it forces code to download and execute on load, which is the cost resumability is meant to avoid. The ESLint plugin ships qwik/no-use-visible-task to flag each use.

A strong candidate reaches for an event handler, useOn, useOnDocument or useOnWindow first, and keeps visible tasks small.

The tasks docs add an edge case: if the component's first rendered element is hidden, the intersection observer never fires and the task does not run. In v2, the old eagerness option on visible tasks is removed.

12. How does task cleanup work, including on the server?

The task callback receives a cleanup function to register teardown work, and you can also return a cleanup function. Cleanup runs before the next run of the task and when the component is removed.

It does not run when a task finishes normally.

useTask$(({ track, cleanup }) => {
  const room = track(() => roomId.value);
  if (isServer) return;
  const ws = new WebSocket(`wss://chat.example.com/rooms/${room}`);
  ws.onmessage = (e) => { messages.value = [...messages.value, e.data]; };
  cleanup(() => ws.close());
});

On the server, cleanup runs after the app has been serialized to HTML, and the cleanup is not transferred to the browser.

That is why the example skips the socket on the server: a connection opened during SSR would close as soon as the response finished.

Since v2 beta, useTask$ and useVisibleTask$ await a returned cleanup promise before the next invocation, so a slow async cleanup now delays the next run.

13. How do context and Slot projection work, and why do they matter for loading?

Context uses createContextId<T>('name'), useContextProvider(id, value) in an ancestor, and useContext(id) in a descendant.

The provided value is usually a store or signal, so it serializes with the rest of the state and descendants subscribe to the properties they read.

<Slot /> projects children into a component without making the child component responsible for rendering them.

Content passed as children belongs to the parent that wrote it, so when that content updates, Qwik re-renders the owning parent and leaves the wrapper component alone.

Candidates coming from React often build wrapper components that read props.children; in Qwik, projection through Slot keeps a layout shell's render code off the client when only its content changes.

Named slots use <Slot name="footer" /> and q:slot="footer" on the projected element.

Qwik City (Qwik Router)

14. How does routing work in Qwik City?

Routes come from the file system under src/routes.

A directory with index.tsx is a page, layout.tsx wraps every page below it, [id] is a dynamic segment, [...rest] is a catch-all, and (group) directories organize files without adding a URL segment.

An index.ts without a default component can act as an endpoint by exporting request handlers.

Request handlers such as onRequest, onGet and onPost exported from layouts and pages run as middleware before loaders and rendering.

They receive a RequestEvent with url, params, cookie, headers, env, sharedMap, redirect, error, json and cacheControl.

A senior candidate can sketch where an auth redirect lives (the top layout's onRequest), and knows that v2 renames Qwik City to Qwik Router, makes the most specific route win regardless of group order, and renders 404.tsx and error.tsx inside their layouts.

15. How do routeLoader$ functions work, and where must they live?

A routeLoader$ runs on the server for each navigation to its route, after middleware and before the components render, and returns data the page reads as a signal.

Per the route loader docs, loaders run on every SPA or MPA navigation, including navigation to the same page, and must be exported from a layout.tsx or index.tsx.

A loader defined in a shared file has to be re-exported from one of those files, or it does not run.

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

export const useProduct = routeLoader$(async (requestEvent) => {
  const res = await fetch(`https://api.example.com/products/${requestEvent.params.id}`);
  if (!res.ok) {
    return requestEvent.fail(404, { errorMessage: 'Product not found' });
  }
  return (await res.json()) as { name: string; price: number };
});

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

Follow-ups worth asking: how one loader reads another with requestEvent.resolveValue(useOtherLoader), how a component in another file reads a loader by importing it from the route file, and why the loader's return value should contain only the fields the page renders, since it is serialized into the HTML.

For v2, the failed check above changes: reading .value of a failed loader throws, and the error sits on the signal.

16. How do routeAction$ and Form work together?

A routeAction$ is a server function that runs only when the client submits to it.

It receives the submitted data and a RequestEvent, can take a zod$() validator as a second argument, and is exported from a layout.tsx or index.tsx like a loader.

The action docs describe <Form> as a native form that works without JavaScript and runs the action in SPA mode when JavaScript is available.

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

export const useSubscribe = routeAction$(
  async ({ email }, requestEvent) => {
    const ok = await addSubscriber(email);
    if (!ok) return requestEvent.fail(409, { message: 'Already subscribed' });
    return { subscribed: true };
  },
  zod$({ email: z.string().email() }),
);

export default component$(() => {
  const action = useSubscribe();
  return (
    <Form action={action}>
      <input name="email" type="email" value={action.formData?.get('email')} />
      {action.value?.fieldErrors?.email && <p>{action.value.fieldErrors.email}</p>}
      {action.value?.failed && <p>{action.value.message}</p>}
      <button type="submit" disabled={action.isRunning}>Subscribe</button>
    </Form>
  );
});

Validation failures land in action.value.fieldErrors, keyed with dot notation for nested fields.

After a submission the server re-renders the page, so route loaders run again and show fresh data without manual refetching. globalAction$ can be declared anywhere in src and suits app-wide actions like login; the docs recommend starting with routeAction$.

In v2, a routeLoader$ can no longer read action state, so a loader that branched on the last action result needs a redesign.

17. When do you use server$ instead of an action?

Use server$ for RPC-style calls from client code that are not form submissions: autocomplete, polling a status, streaming results, or calling a server-only SDK from an event handler.

It wraps a function so it runs only on the server; on the client the generated proxy calls it over HTTP.

Per the server$ docs, the request is available as this, an optional AbortSignal can be passed as the first argument, and an async generator streams values the client reads with for await.

const searchProducts = server$(async function (query: string) {
  const session = this.cookie.get('session')?.value;
  if (!session || !(await isValidSession(session))) {
    throw new Error('Unauthorized');
  }
  const apiKey = this.env.get('SEARCH_API_KEY');
  return search(apiKey, query);
});

Use an action when you want progressive enhancement, form validation with zod$ and automatic loader refresh. The function must be a regular function, not an arrow, for this to work.

The docs also warn that client and server must run the same build, since version skew between them leads to undefined behavior, which matters during rolling deploys.

18. Where do you enforce authentication in a Qwik City app?

Redirect unauthenticated page requests in a layout's onRequest, check permissions again inside each action and server$ that changes data, and put app-wide guards in plugin middleware.

The server$ docs are explicit that layout and page middleware do not run for server function requests, and that the function hash in the URL is not a secret. Plugin files (src/routes/plugin.ts or plugin@name.ts) run before server functions.

// src/routes/plugin@auth.ts
import type { RequestHandler } from '@builder.io/qwik-city';

export const onRequest: RequestHandler = async ({ cookie, sharedMap }) => {
  const token = cookie.get('session')?.value;
  const user = token ? await getUserFromSession(token) : null;
  sharedMap.set('user', user);
};

Loaders, actions and server$ can then read sharedMap.get('user') and decide.

A strong candidate separates authentication (who the user is, set once per request) from authorization (whether this user may act on this record, checked in each handler), and does not trust IDs from the form when the session already identifies the user (question 22).

19. How do you choose between SSR, static generation and edge rendering?

Qwik City renders on the server by default and ships deployment adapters you add with npm run qwik add, which lists options such as Cloudflare Pages, Netlify Edge, Vercel Edge, Node servers like Express and Fastify, and a static site adapter.

The deployment docs cover each one. Static generation prerenders routes at build time; dynamic routes list their parameters with an exported onStaticGenerate.

Questions that separate seniors:

  • How they cache SSR output: cacheControl in onGet with maxAge, staleWhileRevalidate and an sMaxAge for the CDN, and why personalized pages must stay private.
  • What breaks on edge runtimes: Node-only libraries, long-lived database connections, and request time limits.
  • Why a static page still serializes state and can still resume, so it stays interactive without a server.
  • How streaming SSR affects time to first byte when a loader is slow, and what v2's <Pending> boundaries change (question 31).

Code Review Rounds

The snippets below use Qwik 1.x imports. Give the candidate five minutes per snippet and ask for the bug, what the user sees, and the fix.

20. Find the bug: a profile form that never updates.

export const Profile = component$(() => {
  const user = useSignal({ name: 'Ana', plan: 'free' });

  return (
    <div>
      <p>{user.value.name} is on the {user.value.plan} plan</p>
      <button onClick$={() => { user.value.plan = 'pro'; }}>Upgrade</button>
    </div>
  );
});

Bug: the click mutates a property of the object held in the signal.

The signal tracks assignments to .value, and user.value.plan = 'pro' reads .value and then writes to a plain object, so no subscriber is notified and the text still says "free".

The data changed in memory, which makes the bug confusing in a debugger.

Fix: assign a new object, user.value = { ...user.value, plan: 'pro' }, or switch to useStore({ name: 'Ana', plan: 'free' }) and write user.plan = 'pro'.

Strong candidates pick based on how the data changes: a store when fields change one at a time, a signal when the whole object is replaced from the server.

21. Find the bug: a loader that never runs.

// src/components/order-summary.tsx
export const useOrder = routeLoader$(async ({ params }) => {
  return getOrder(params.orderId);
});

export const OrderSummary = component$(() => {
  const order = useOrder();
  return <p>Total: {order.value.total}</p>;
});

// src/routes/orders/[orderId]/index.tsx
import { OrderSummary } from '~/components/order-summary';

export default component$(() => <OrderSummary />);

Bug: the loader is declared in a component file and never exported from the route's index.tsx or a layout. Qwik City discovers loaders by reading the exports of route files, so this one is not registered and does not run.

The route loader docs warn that a shared loader that is not re-exported "will not run or throw exception."

Fix: add export { useOrder } from '~/components/order-summary'; to the route file. The ESLint plugin's qwik/loader-location rule flags loaders declared outside route files.

A senior candidate also asks whether getOrder checks that the order belongs to the signed-in user, since orderId comes from the URL.

22. Find the bugs: an action that changes a user's email.

export const useChangeEmail = routeAction$(async (data) => {
  await db.users.update({
    where: { id: data.userId as string },
    data: { email: data.email as string },
  });
  return { ok: true };
});

export default component$(() => {
  const session = useSession();
  const action = useChangeEmail();
  return (
    <Form action={action}>
      <input type="hidden" name="userId" value={session.value.userId} />
      <input name="email" />
      <button>Save</button>
    </Form>
  );
});

Bugs:

  • Broken authorization: the action trusts userId from the form. Anyone can post a different ID and change another account's email, which is an account takeover if email drives password reset.
  • No validation: without zod$, email can be missing, an array or any string, and the casts hide that from TypeScript.
  • No authentication check inside the action. A layout redirect protects the page render; the action handler should still confirm a session exists.

Fix: read the user from the session in the action's RequestEvent (or from sharedMap set by plugin middleware), drop the hidden field, add zod$({ email: z.string().email() }), and return fail(401, ...) when there is no session.

23. Find the bug: a dashboard that fetches on load.

export default component$(() => {
  const stats = useSignal<Stats | null>(null);

  useVisibleTask$(async () => {
    const res = await fetch('/api/stats');
    stats.value = await res.json();
  });

  return stats.value ? <StatsGrid stats={stats.value} /> : <Spinner />;
});

Bug: it works, and it throws away what Qwik is for.

The server renders a spinner, the browser downloads and runs the task as soon as the component is visible, waits for a second network round trip, then downloads StatsGrid's render code and re-renders.

The user sees a spinner and a layout shift, and search engines see no data.

Fix: load the stats with a routeLoader$ so the server renders the grid into the HTML and no task runs on the client.

If the data must refresh in the browser, track a signal in useTask$ or use useResource$ (async useComputed$ in v2), triggered by user action.

A strong candidate mentions qwik/no-use-visible-task and explains the extra round trip in terms of what the network panel shows.

24. Find the bug: an admin function behind a layout guard.

// src/routes/admin/layout.tsx
export const onRequest: RequestHandler = async ({ cookie, redirect }) => {
  if (!(await isAdmin(cookie.get('session')?.value))) {
    throw redirect(302, '/login');
  }
};

// src/routes/admin/users/index.tsx
const deleteUser = server$(async function (id: string) {
  await db.users.delete({ where: { id } });
});

export default component$(() => (
  <button onClick$={() => deleteUser('u_123')}>Delete</button>
));

Bug: the developer assumes the admin layout's onRequest protects everything under /admin. It protects page requests.

The server$ docs state that layout and page middleware do not run for server function requests, and that the function's endpoint hash is not secret. Anyone who finds the endpoint in a client chunk can call it and delete users.

Fix: check the session inside deleteUser through this.cookie, or populate the user in plugin middleware (which runs before server functions) and check the role in the function. Validate id too.

Strong candidates also suggest a routeAction$ for destructive operations, so it uses a form submission with validation and returns a fail result the page can show.

25. Find the bug: a client captured in a handler.

class ApiClient {
  constructor(private baseUrl: string) {}
  async like(postId: string) {
    await fetch(`${this.baseUrl}/posts/${postId}/like`, { method: 'POST' });
  }
}

export const LikeButton = component$((props: { postId: string }) => {
  const api = new ApiClient('https://api.example.com');
  return <button onClick$={() => api.like(props.postId)}>Like</button>;
});

Bug: the click handler captures api, an instance of a custom class. During SSR, Qwik has to serialize every captured value into the page so the handler can run after resume, and class instances are not serializable.

The serialization guide calls out this case: it passes static analysis and fails at runtime.

Fix: move the instance to module scope (export const api = new ApiClient(...)), which the extracted handler can import, or create it inside the handler.

If the object must be shared and stateful, keep plain data in a store and wrap the instance with noSerialize for client-only use.

The candidate should explain that the handler will run in a different module, later, with only serialized state, which is why the rule exists.

Performance, Testing and Integrations

26. A Qwik page feels slow on first click. How do you investigate?

Start in the network panel with cache disabled, click, and look at what loads: which chunks, in what order, and whether any were preloaded.

A slow first click usually means the handler's chunk was not preloaded, or the handler imports a large dependency, or the click changes state that forces a large component to download its render code and re-render.

  • Check the build's bundle graph and preload hints to confirm the handler's chunk is in it.
  • Move heavy imports to a dynamic import() inside the handler so the handler itself stays small.
  • Push reactive reads down into small components (question 8) so a state change re-renders a leaf, not the page.
  • Search for useVisibleTask$ uses that run on load and compete with the click for bandwidth.
  • Check serialized state size in the HTML; large loader payloads slow the initial parse.

A strong candidate measures Interaction to Next Paint in the field rather than relying on a lab run, because a warm cache hides the cost of the first download.

27. How do you test Qwik components and routes?

Use Vitest with createDOM() from @builder.io/qwik/testing for component tests: it renders a component into a test DOM and provides userEvent to dispatch events and wait for the resulting renders.

Use Playwright for end-to-end tests that cover routing, loaders, actions and resumability on a real server build, since only a production build exercises the Optimizer's chunking.

Two details a candidate who has written Qwik tests will know. Tests that only render on the client never exercise serialization, so bugs like question 25 slip past unit tests and show up in SSR.

And v2 changes the test helpers: per the migration guide, trigger() returns the dispatched Event and waits for idle by default, event.target is null under trigger() (read the element from the handler's second argument), and tests should create fresh DOM rather than re-render into an existing container.

28. How do you use React components inside a Qwik app, and what does it cost?

The qwik-react integration wraps a React component with qwikify$() so it renders on the server and hydrates as an island on the client. The integrations docs cover setup through npm run qwik add react.

Props such as client:visible, client:idle, client:hover and client:only control when the island hydrates.

The cost is that each hydrated island downloads React and runs the component on the client, which is the hydration Qwik otherwise avoids.

Strong candidates use it for a contained widget with no Qwik equivalent, such as a date picker or a chart library, hydrate it on visibility or interaction, and avoid passing large objects through the boundary.

A weak answer wraps whole pages in qwikify$ during a migration and then wonders why the bundle matches the old React app. In v2 the package is @qwik.dev/react.

What Changed Recently

Aug 21, 2025
Qwik 1.16.0 moves to Vite 7
Dec 17, 2025
Qwik 1.18.0: computed signals notify listeners only when their value changes
Jan 30, 2026
Qwik 1.19.0: untrack() accepts signals and stores directly
Jul 16, 2026
v2 beta.38: async useComputed$, with useAsync$ deprecated in its favor
Aug 21, 2026
v2 beta.40 requires Vite 8 and Rolldown
Sep 23, 2026
Qwik 1.20.1: qwik migrate-v2 rewrites APIs, config and the Vite version
Oct 1, 2026
Qwik 2.0.0-rc.0, the first release candidate
Oct 25, 2026
Qwik 1.20.3 and 2.0.0-rc.4 ship the same day

29. What is the status of Qwik v2, and should a team adopt it now?

Qwik v2 is in release candidates. The first, 2.0.0-rc.0, shipped October 1, 2026, and the latest is 2.0.0-rc.4 (October 25, 2026); GitHub marks each v2 release as a prerelease, while the 1.x line continues with 1.20.3.

The RC announcement asks teams to try it and report problems, names no final release date, and lists work left before 2.0: testing the RC in more apps, stabilizing experimental APIs and finishing the v2 docs.

The post claims v2 is more than twice as fast as v1 at reactivity, with a js-framework-benchmark weighted geometric mean of 1.23 against 2.59 for v1, and it notes those results are not yet published and may change.

It also marks <Pending>, <Catch>, <Each> and <Show> as experimental.

A sound answer for a production team in November 2026: stay on 1.20.x for shipped apps, run migrate-v2 on a branch to size the work, and start new greenfield projects on the RC only if the team can absorb RC churn.

One npm detail trips people up: the latest dist-tag of @qwik.dev/core points at 2.0.0-rc.4, because that package has no stable release yet.

30. What breaks when you migrate from Qwik 1.x to v2?

The v2 migration guide starts with the CLI: npx @qwik.dev/core@latest migrate-v2 (or qwik migrate-v2 inside a 1.x project, expanded in 1.20.1) handles package renames, identifier changes and config updates.

The rest is behavior that compiles and breaks at runtime.

  • Packages: @builder.io/qwik becomes @qwik.dev/core, @builder.io/qwik-city becomes @qwik.dev/router, and @builder.io/qwik-react becomes @qwik.dev/react. Third-party v1 libraries need package-manager overrides.
  • Router names: QwikCityProvider becomes QwikRouterProvider, or the useQwikRouter() hook; qwikCity becomes qwikRouter in the Vite config.
  • Build: Vite 8 with Rolldown is required, Rollup manualChunks is not supported, and the CJS and UMD builds are gone.
  • Loaders: route loaders are async signals, value.failed is gone, a loader cannot read action state, and caching uses a new cacheControl option.
  • Router behavior: SPA view transitions are opt-in through viewTransition on QwikRouterProvider, and 404.tsx renders inside its layout unless renamed 404!.tsx.
  • Deprecated APIs: useResource$ and <Resource> give way to async useComputed$ with <Pending> and <Catch>, and polling moves to usePoll from @qwik.dev/utils.
  • Error handling: useErrorBoundary() is removed; <ErrorBoundary> lives in @qwik.dev/core behind an experimental flag.

A strong candidate names the loader change first, because if (loader.value.failed) checks fail silently in review and throw in production.

31. How does async useComputed$ work in v2, and what does Pending add?

In v2, useComputed$ accepts an async function and receives a compute context with track() and an abortSignal.

Per the migration guide, reading .value throws while the value is unresolved and re-throws if the computation failed, so components read it inside <Pending> and <Catch> boundaries or supply an initial value.

Reads after an await must go through track(), because the synchronous tracking context ends at the first await.

import { component$, useComputed$, useSignal } from '@qwik.dev/core';

export const UserCard = component$(() => {
  const userId = useSignal(1);
  const user = useComputed$(async ({ track, abortSignal }) => {
    const id = track(() => userId.value);
    const res = await fetch(`/api/users/${id}`, { signal: abortSignal });
    return (await res.json()) as { name: string };
  });

  return <p>{user.value.name}</p>;
});

A change to userId runs the computation again, and passing abortSignal to fetch lets the stale request be cancelled instead of racing the new one.

The RC post describes <Pending> as the basis of out-of-order streaming: the server sends the page with fallbacks and streams slow sections later.

A senior candidate notes the experimental label on <Pending> and <Catch>, and that the 2.0.0-beta.46 changelog moved the fallback prop to fallback$, so code written against early betas needs updating.

Signs of a Strong Answer

  • They describe what the HTML carries after SSR (state, listener attributes, QRLs) and what the browser downloads on the first click.
  • They spot captures across $ boundaries by reading the code, and say which ones fail at runtime instead of at build time.
  • They reach for routeLoader$ before any client fetch, and treat each useVisibleTask$ as a cost to justify.
  • They put authorization inside each action and server$, and know that layout middleware does not cover server functions.
  • They keep reactive reads in small leaf components so a state change does not pull a page's render code onto the client.
  • They can say which Qwik version they run, that v2 is at 2.0.0-rc.4, and which of their loaders and resources the migration would touch.

Hiring Qwik Developers

Engineers who have shipped Qwik in production, debugged serialization errors under SSR and tuned first-interaction latency are rare on general job boards, so most teams hire strong TypeScript front-end engineers and screen them with questions like these.

Second Talent helps US startups hire vetted AI-Native Engineers from Asia, including front-end developers and TypeScript developers. For related screens, see our SolidJS, TypeScript and front-end developer interview guides.

Rates run $23 to $39 USD an hour, billed monthly, with matches in 24 hours and a 90-day one-time replacement guarantee. Compare budgets on the cost to hire a front-end developer page, or tell us what you need.

Get developers who ship with AI agents.

Starts at $23/Hour.

Start Hiring

$0 cost until you hire