Qwik Interview Questions and Answers

Last updated:

Check out 35 of the most common Qwik interview questions, then take an AI-powered practice interview

JavaScriptTypeScriptResumabilityLazy LoadingPerformance
35+
Questions
14
Basic
14
Intermediate
7
Advanced
Q1

What does resumability mean in Qwik, and how is it different from hydration?

BasicCore Model

Answer

Hydration is the recovery step every SSR framework runs on the client: download the component code, execute every component function again, rebuild the virtual DOM or the reactive graph in memory, compare it against the server HTML, and finally attach event listeners. The cost grows with the size of the application, so a large React or Vue page pays for hydration even if the user never interacts with 95 percent of it. Resumability removes that step.

During SSR, Qwik writes three things into the HTML: the serialised state (in a script block of type qwik/json), the listener locations (as on:click and similar attributes whose value points at a chunk plus a symbol name), and the subscription graph that says which signal feeds which piece of DOM. On load the browser runs only qwikloader, about a kilobyte of inline JavaScript, which registers a small number of delegated listeners on the document and then does nothing. No component function runs.

When the user clicks, qwikloader reads the attribute, dynamically imports that one chunk, deserialises just the state that closure captured, and calls the handler. Startup work is therefore roughly constant regardless of application size, which is the claim interviewers will ask you to defend. The honest trade-off is that the first interaction now needs a network fetch, and Qwik pays for that with aggressive service-worker prefetching. Look at the root element in dev tools: it carries q:container='paused' after SSR and flips to 'resumed' once qwikloader takes over.

<!-- What Qwik actually ships for a counter button -->
<html q:container='paused' q:version='1.x' q:base='/build/'>
  <body>
    <button on:click='q-a1b2c3.js#s_0Xk2pQr[0]'>Count: 3</button>

    <script type='qwik/json'>
      {"objs":[{"value":3}],"subs":[["2 #0"]]}
    </script>
    <script id='qwikloader'>/* ~1KB, registers delegated listeners */</script>
  </body>
</html>

<!-- Nothing else executes until the click.
     [0] is an index into objs, so only that signal is deserialised. -->

Key Points

  • Hydration re-executes components on load; resumability never does
  • State, listeners and the subscription graph are serialised into HTML
  • qwikloader is roughly 1 KB and uses delegated listeners on document
  • Startup cost is close to O(1) in app size, not O(n)
  • Trade-off moves to interaction time, mitigated by prefetching
๐Ÿ’ก Pro Tip: When an interviewer asks whether resumability is just lazy hydration, say no and explain why: lazy hydration still eventually runs component code, resumability never runs it again on the client.
Q2

What does the $ suffix mean in component$, useTask$ and onClick$, and what is a QRL?

BasicCore Model

Answer

The dollar sign is not a naming convention, it is a compiler instruction. Every function you pass to a dollar-suffixed API becomes a lazy-loading boundary: the Qwik optimizer lifts that closure out of your file into its own exported symbol in a separate chunk, and replaces the call site with a QRL. A QRL (Qwik URL) is a serialisable pointer with three parts: the chunk path, the symbol name inside that chunk, and the list of captured lexical variables.

It survives being written into an HTML attribute, which is exactly what makes resumability possible. You can create your own boundary with the bare $() function, and you can type a prop that accepts one as QRL<(id: string) => void>. Because a QRL is a pointer rather than a function, calling it is asynchronous: qrl() returns a promise, and you either await it or call qrl.resolve() first.

The rules that follow from this are what interviews probe. The closure must be able to serialise everything it captures, so no class instances holding sockets, no let bindings, no functions that were not themselves wrapped in $(). The dollar function must be called at the top level of a component or module, never conditionally inside an if block, because the optimizer performs a static transform and cannot follow runtime branches. Every dollar boundary is also a potential HTTP request at interaction time, so scattering hundreds of tiny handlers across a page has a real cost that prefetching, not cleverness, has to absorb.

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

interface Props {
  // A prop that accepts a lazy-loadable function
  onPick$: QRL<(id: string) => void>;
}

export const Row = component$<Props>(({ onPick$ }) => {
  return <button onClick$={() => onPick$('row-7')}>Pick</button>;
});

export const Parent = component$(() => {
  const picked = useSignal('');

  // $() creates a standalone lazy boundary you can pass around
  const handlePick = $((id: string) => {
    picked.value = id;
  });

  return (
    <>
      <Row onPick$={handlePick} />
      <p>Picked: {picked.value}</p>
    </>
  );
});

Key Points

  • $ marks a lazy-loading boundary, resolved at build time by the optimizer
  • A QRL stores chunk path, symbol name and captured variables
  • QRLs resolve asynchronously; use await or qrl.resolve()
  • Type function props as QRL<...> so callers must pass a $ boundary
  • Never call $() conditionally; the transform is static
Q3

What does the Qwik optimizer do to your source at build time, and where do the s_ symbols come from?

BasicBuild and Tooling

Answer

The optimizer is a Rust transform (built on SWC) that ships inside @builder.io/qwik/optimizer and runs as part of the qwikVite() plugin. For every dollar-suffixed call it extracts the closure into a new module, gives it a stable hashed export name of the form s_<hash>, records which lexical variables the closure captured, and rewrites the original call to the Qrl variant: component$(fn) becomes componentQrl(qrl('./chunk', 's_hash', [captured])). Because each extracted symbol lives in its own entry, Rollup can emit it as an independently loadable chunk instead of bundling everything a page might need into one file.

The output of a production build is therefore dozens or hundreds of very small chunks plus a q-manifest.json that maps every symbol to its chunk, its parent component, and rough usage hints. That manifest is fed back into the SSR render so the generated HTML can write the correct chunk name into each on:click attribute, and it is also what the bundle graph and prefetcher read. Two practical consequences come up in interviews.

First, symbol names are content-derived, so changing the body of a handler changes its hash and its chunk, which matters for CDN caching and for pages that were served before a redeploy. Second, the transform is purely static: you cannot generate dollar boundaries dynamically, and code that hides a handler behind a runtime condition will either fail the eslint rules or silently break serialisation. Running the dev server with debug output or inspecting dist/build after npm run build is the fastest way to see the split for real.

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

// The optimizer emits roughly this (simplified)

// counter.tsx
import { componentQrl, qrl } from '@builder.io/qwik';
export const Counter = componentQrl(qrl(() => import('./counter_component_a1b2'), 's_a1b2'));

// counter_component_a1b2.js
export const s_a1b2 = () => {
  const count = useSignal(0);
  return <button onClick$={qrl(() => import('./counter_click_c3d4'), 's_c3d4', [count])}>{count.value}</button>;
};

// counter_click_c3d4.js
export const s_c3d4 = () => {
  const [count] = useLexicalScope();
  count.value++;
};
๐Ÿ’ก Pro Tip: If a reviewer asks why Qwik needs a Rust compiler at all, the answer is chunk granularity: hand-splitting a codebase into one chunk per event handler is not something a human maintains.
Q4

How does qwikloader wire up events, and what do the q: attributes in the HTML mean?

BasicCore Model

Answer

qwikloader is the only script Qwik guarantees to run on page load. It is inlined into the HTML, weighs about a kilobyte minified and gzipped, and its whole job is event delegation. During SSR, Qwik scans the rendered tree for the event types actually used and qwikloader registers one capture-phase listener per event type on the document, not one per element.

When an event fires it walks up from the target looking for a matching on:<event> attribute, reads the value (a chunk path plus a symbol, optionally with capture indexes such as q-a1b2.js#s_x7[0 3]), resolves it against the container's q:base URL, dynamically imports the chunk, restores the captured lexical scope from the serialised state, and invokes the symbol. The surrounding q: attributes carry the container metadata: q:container is 'paused' after SSR and 'resumed' once qwikloader has taken over, q:base is the URL prefix that chunk paths are resolved against (critical when your build output lives on a CDN under a different origin), q:version identifies the runtime, q:manifest-hash ties the HTML to a specific build, and q:id / q:key are used to reconcile serialised state with DOM nodes. There are also document- and window-level variants (document:onKeyDown$ compiles to an on-document: attribute) plus a synthetic qvisible event backed by IntersectionObserver, which is how useVisibleTask$ with the default eagerness fires. Two failure modes to name in an interview: a wrong q:base after moving assets to a CDN, which produces silent 404s on first click, and content-security policies that block the inline qwikloader script, which leaves the page completely inert.

// Handlers you can attach without markup, useful for document/window events
import { component$, useOnDocument, $ } from '@builder.io/qwik';

export const Shortcuts = component$(() => {
  useOnDocument(
    'keydown',
    $((e: KeyboardEvent) => {
      if (e.key === 'k' && e.metaKey) console.log('open palette');
    })
  );
  return <div>Press Cmd+K</div>;
});

// Rendered HTML carries the listener on the container element:
// <div on-document:keydown='q-9f2.js#s_keydown'>Press Cmd+K</div>

Key Points

  • One delegated capture-phase listener per event type on document
  • on:click attribute value is chunk#symbol plus capture indexes
  • q:base resolves chunk URLs; wrong value equals dead buttons
  • q:container flips from paused to resumed
  • qvisible is a synthetic event driven by IntersectionObserver
Q5

When should you use useSignal versus useStore, and what breaks if you pick wrong?

BasicState

Answer

useSignal holds a single value behind a .value accessor and is the default choice for anything primitive: a counter, a boolean flag, an input string, a DOM element reference. useStore returns a reactive Proxy around an object and is for grouped state where you want to mutate fields in place. Both are serialisable, both participate in the subscription graph, and both work across the resumability boundary, but the reactivity granularity differs. Reading signal.value inside JSX subscribes only that text node or attribute, so updating it patches the DOM directly without re-running the component function.

A store subscribes per property read, which is powerful but easy to get wrong with nesting: a store tracks its top-level properties, and nested objects or arrays need the deep option before mutations inside them notify subscribers. The safest habit is to pass { deep: true } explicitly whenever the store contains nested structures rather than relying on the default, because the failure mode is silent, the mutation happens and the UI simply does not update. The second classic bug is destructuring.

Writing const { count } = store copies the value out of the proxy, the read is no longer tracked, and the component stops reacting. Keep the store.count form in JSX. Signals have the same rule in reverse: passing sig around is fine because the proxy travels with it, passing sig.value snapshots it. In an interview, mention that both hooks serialise their contents into the HTML, so putting a 400 KB API response into a store inflates your document and hurts the very metric you chose Qwik for.

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

export const Form = component$(() => {
  const email = useSignal('');                       // primitive: signal
  const state = useStore(
    { touched: false, errors: { email: '' } },        // nested object
    { deep: true }                                    // required for errors.email
  );

  return (
    <>
      <input
        value={email.value}
        onInput$={(_, el) => {
          email.value = el.value;
          state.touched = true;
          state.errors.email = el.value.includes('@') ? '' : 'Invalid email';
        }}
      />
      {state.touched && state.errors.email && <p>{state.errors.email}</p>}
    </>
  );
});
๐Ÿ’ก Pro Tip: Never destructure a store or props in Qwik. It is the single most common reactivity bug and interviewers plant it in code-reading rounds.
Q6

Why can't you pass an ordinary callback as a prop in Qwik, and how do you type one that works?

BasicComponents

Answer

Props in Qwik have to survive serialisation, because the child component may resume in the browser long after the parent's code has been discarded. A plain arrow function cannot be written into HTML, so passing one as a prop either fails the eslint rule qwik/valid-lexical-scope at build time or throws a serialisation error at runtime. The fix is to make the prop a QRL.

By convention you name it with a dollar suffix (onSelect$, onSave$) and type it as QRL<(arg: T) => void>, which forces every caller to wrap the implementation in $() or to pass an inline arrow that the optimizer can extract at the call site. Inside the child, calling the prop is asynchronous because resolving a QRL may trigger a dynamic import, so you either await it or fire and forget. Everything else about props follows the same serialisation rule: primitives, plain objects, arrays, Date, URL, Map, Set, signals and stores travel fine, while class instances holding connections, DOM nodes created outside Qwik, or third-party SDK objects do not.

Props are also immutable in Qwik. You do not mutate props to communicate upward, you either call a QRL callback or share a signal between parent and child, which is often the cleaner pattern because it avoids a chunk fetch entirely: the child writes sig.value and the parent's DOM updates through the subscription graph without any component re-render. Interviewers like asking which of those two designs you would pick and why.

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

// Pattern A: QRL callback prop
interface ItemProps {
  label: string;
  onSelect$: QRL<(label: string) => void>;
}
export const Item = component$<ItemProps>((props) => (
  <li onClick$={() => props.onSelect$(props.label)}>{props.label}</li>
));

// Pattern B: shared signal, no chunk fetch on click of the parent side
export const Chip = component$<{ selected: Signal<string>; label: string }>((props) => (
  <li onClick$={() => (props.selected.value = props.label)}>{props.label}</li>
));

export const List = component$(() => {
  const selected = useSignal('');
  return (
    <ul>
      <Item label='Mumbai' onSelect$={$((l) => (selected.value = l))} />
      <Chip selected={selected} label='Pune' />
      <p>Selected: {selected.value}</p>
    </ul>
  );
});
Q7

What does useTask$ do, when does it run on the server versus the client, and what is track() for?

BasicLifecycle

Answer

useTask$ registers work that runs before render and, unlike React's useEffect, it runs on the server during SSR. On the first pass the task executes eagerly on the server, before the component's markup is produced, so anything it writes into signals or stores is already reflected in the HTML that reaches the browser. After that, the task re-runs only when a value it explicitly tracked changes, and that re-run happens wherever the change happens, which is usually the client.

Dependencies are declared with the track function passed into the task: calling track(() => count.value) subscribes the task to that signal. Reading a signal without track does not create a subscription, which is the opposite of Solid and a frequent source of confusion, so a task that never calls track runs exactly once. Tasks can be async and can return a cleanup function through the cleanup callback, which fires before the next run and on component teardown, making it the right home for clearing timers and aborting fetches.

Two rules matter in production. First, useTask$ blocks rendering, so a slow await inside it delays your server response and hurts TTFB; move data loading into routeLoader$ or useResource$ instead. Second, useTask$ is not a browser-only hook, so touching document or window inside it crashes SSR with a ReferenceError. If you genuinely need the DOM, that is what useVisibleTask$ exists for, and interviewers will check that you know the difference rather than reaching for the client hook by reflex.

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

export const Search = component$(() => {
  const query = useSignal('');
  const debounced = useSignal('');

  useTask$(({ track, cleanup }) => {
    track(() => query.value);          // subscribe explicitly
    const id = setTimeout(() => (debounced.value = query.value), 300);
    cleanup(() => clearTimeout(id));   // runs before next execution
  });

  return (
    <>
      <input onInput$={(_, el) => (query.value = el.value)} />
      <p>Searching for: {debounced.value}</p>
    </>
  );
});

Key Points

  • Runs on the server first, before render, then on tracked changes
  • track() is mandatory; a plain read does not subscribe
  • cleanup() fires before re-run and on teardown
  • Blocks SSR output, so avoid slow awaits inside it
  • No document or window access; that crashes SSR
Q8

What is useVisibleTask$, and why does the Qwik eslint plugin warn every time you use it?

BasicLifecycle

Answer

useVisibleTask$ is the explicit escape hatch for browser-only code. It never runs on the server, and by default it fires when the component's host element becomes visible, driven by the synthetic qvisible event that qwikloader wires to an IntersectionObserver. The eagerness option changes that timing: 'visible' is the default, 'idle' waits for requestIdleCallback, and 'load' runs as soon as the document is interactive.

It is the correct place for anything that needs the real DOM or a browser API: initialising a chart library, measuring an element, opening a WebSocket, registering a ResizeObserver. The eslint plugin flags it with qwik/no-use-visible-task because it is the one API that reintroduces eager client JavaScript into a framework whose entire premise is not shipping any. Each visible task forces a chunk download and execution without the user asking for anything, and a codebase that sprinkles them everywhere gets React-shaped performance with Qwik-shaped complexity, which is the worst of both.

The warning exists so that adding one becomes a deliberate act, usually accompanied by an eslint-disable-next-line comment and a short justification in the code. In interviews the follow-up is always the same: what would you use instead? Good answers include useTask$ for state derivation, routeLoader$ or useResource$ for data, useOn for lazily attached event listeners, and CSS or the native loading attribute for anything that does not truly need JavaScript. Reserve the visible task for genuine imperative browser integrations, and keep it as far down the tree as possible so only the components that need it pay.

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

export const Chart = component$(() => {
  const host = useSignal<HTMLDivElement>();

  // eslint-disable-next-line qwik/no-use-visible-task
  useVisibleTask$(async ({ cleanup }) => {
    const { Chart } = await import('chart.js/auto'); // browser-only lib
    const chart = new Chart(host.value!.querySelector('canvas')!, config);
    cleanup(() => chart.destroy());
  }, { strategy: 'intersection-observer' });

  return (
    <div ref={host}>
      <canvas />
    </div>
  );
});
๐Ÿ’ก Pro Tip: If you write more than two or three useVisibleTask$ calls in an app, treat it as a design smell and re-check whether the work can move to the server or to a lazy event handler.
Q9

How do you scaffold a Qwik project, and what do the build scripts actually produce?

BasicBuild and Tooling

Answer

You start with npm create qwik@latest, which asks whether you want the empty starter or the Qwik Router (formerly Qwik City) app, and scaffolds a Vite project with TypeScript, eslint including eslint-plugin-qwik, and Prettier already configured. The tree matters: src/routes holds file-based routes, src/components holds components, src/root.tsx is the document shell, src/entry.ssr.tsx configures renderToStream, and src/entry.dev.tsx and entry.preview.tsx cover the other modes. npm start runs vite in SSR dev mode with the optimizer in dev configuration, so chunks are split lazily but not minified. npm run build is the important one: it chains build.client (the browser bundles plus q-manifest.json) and build.server (the SSR entry), and npm run preview serves the production build locally so you can see the real chunk-splitting behaviour, which dev mode deliberately hides. The other command you will be asked about is npm run qwik add, an interactive integration installer that patches your Vite config and adds files for you: adapters such as cloudflare-pages, vercel-edge, netlify-edge, express and static, and integrations such as tailwind, playwright, vitest, partytown and react.

Because it edits config in place, run it on a clean git tree. For type safety, npm run build.types runs tsc and Qwik Router generates route typings so useLocation params are typed. A useful interview detail: never benchmark Qwik in dev mode. Dev serves unbundled modules through Vite, so the network waterfall looks nothing like production and people routinely conclude Qwik is slow because they measured the wrong thing.

# Scaffold
npm create qwik@latest
cd my-app && npm install

# Dev server (SSR mode, unminified, do not benchmark this)
npm start

# Production build: client bundles + q-manifest.json, then the SSR entry
npm run build          # runs build.client && build.server && build.types
npm run preview        # serve the real production output locally

# Add integrations (patches vite.config.ts in place)
npm run qwik add cloudflare-pages
npm run qwik add tailwind
npm run qwik add playwright
Q10

How does file-based routing work in Qwik Router, including layouts, params and route groups?

BasicRouting

Answer

Routes live under src/routes and the directory structure is the URL structure. A folder becomes a path segment, and index.tsx inside it is the page component for that path. layout.tsx wraps every route in its folder and below it, and layouts nest, so src/routes/layout.tsx is the app shell and src/routes/dashboard/layout.tsx wraps only the dashboard subtree. Inside a layout you render children with the Slot component.

Dynamic segments use square brackets: [id] captures one segment and is available through useLocation().params.id, while [...rest] is a catch-all that captures everything remaining. Parentheses create route groups, so (marketing)/pricing/index.tsx serves /pricing while letting you give the marketing pages their own layout without adding a URL segment. Named layouts use the at-sign form, layout-wide.tsx paired with index@wide.tsx, when one page in a folder needs a different shell.

Beyond page components, a route file can export server-side handlers: onGet, onPost, onRequest, plus head for per-route document metadata. A folder containing only index.ts with those handlers becomes an API endpoint rather than a page. Two details interviewers check: routes are matched statically, so there is no runtime route table you can mutate, and file names that look like routes but are not, such as anything prefixed with an underscore or placed outside src/routes, are ignored by the router. Also remember 404 handling and error boundaries: a 404.tsx or a thrown error from a loader flows through the router rather than through client-side code.

src/routes/
  layout.tsx                  -> shell for every page (renders <Slot/>)
  index.tsx                   -> /
  (marketing)/
    layout.tsx                -> shell for marketing pages only
    pricing/index.tsx         -> /pricing
  jobs/
    index.tsx                 -> /jobs
    [slug]/index.tsx          -> /jobs/frontend-engineer
  docs/[...path]/index.tsx    -> /docs/a/b/c
  api/health/index.ts         -> GET /api/health (onGet handler, no page)

// src/routes/layout.tsx
import { component$, Slot } from '@builder.io/qwik';
export default component$(() => (
  <main>
    <Nav />
    <Slot />
  </main>
));
Q11

What is routeLoader$, where must it be declared, and how does its data reach the browser?

BasicData Loading

Answer

routeLoader$ is the standard way to fetch data for a page. You declare it at module scope in a route file (index.tsx or layout.tsx under src/routes) and export it with a use-prefixed name, because the eslint rule qwik/loader-location enforces both the location and the naming. The function runs on the server only, receives a RequestEvent giving you params, query, cookies, env and the shared map, and its return value is serialised into the response.

In a component you call the exported hook and get back a readonly Signal, so you read loader.value in JSX and the value participates in the reactive graph like any other signal. Because it runs on the server it is the correct place for database queries, secrets and private API keys: none of that code is ever sent to the browser. All loaders for a matched route run in parallel, and layout loaders run alongside page loaders, so a nav bar loader does not serialise behind the page loader.

During a client-side navigation the router fetches the loader payload as a q-data.json request for the target route rather than a full HTML document. Loaders can throw redirect(302, '/login') or fail(404, {...}) to control the response, and a loader can await another loader's value using its hook inside the loader itself, which is how you compose auth checks. The common mistake to name in an interview: returning a huge object. Everything a loader returns is serialised into the HTML, so paginate and project down to the fields the page actually renders.

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

export const useJob = routeLoader$(async ({ params, status, env }) => {
  const res = await fetch(`https://api.example.com/jobs/${params.slug}`, {
    headers: { authorization: `Bearer ${env.get('API_KEY')}` },
  });
  if (!res.ok) {
    status(404);
    return null;
  }
  const job = await res.json();
  return { title: job.title, city: job.city, ctc: job.ctc }; // project, do not dump
});

export default component$(() => {
  const job = useJob();
  if (!job.value) return <p>Job not found</p>;
  return <h1>{job.value.title} in {job.value.city}</h1>;
});
๐Ÿ’ก Pro Tip: Loaders are server-only but their return value is not. Anything you return ends up in the HTML, so never return a full user record with password hashes or internal ids.
Q12

How do Link, useNavigate and useLocation work, and when does Qwik do an SPA navigation versus a full page load?

BasicRouting

Answer

Link is Qwik Router's anchor replacement. It renders a real <a> element so it works without JavaScript and remains crawlable, but when the router is active it intercepts the click and performs an SPA navigation: it fetches the target route's q-data.json, runs the loaders on the server, and patches the DOM in place while preserving the current container state. Plain <a href> always causes a full document load, which is sometimes what you want, for example when leaving to a different container or a non-Qwik page. useNavigate() returns an async nav function for programmatic navigation, typically after a form action or a login redirect, and it accepts options such as { replaceState: true } and a forceReload flag. useLocation() returns a reactive object with url, params, isNavigating and the previous URL, and isNavigating is what you bind to a loading indicator so users get feedback while loaders run.

Link also supports prefetch behaviour: hovering or scrolling a Link into view lets the service worker warm the chunks and the q-data payload for that route, which is why Qwik navigations often feel instant. Two things worth naming in an interview. First, SPA navigation reruns loaders but does not re-execute component code that was never executed in the first place, so the resumability model holds across navigations. Second, MPA mode is a legitimate choice: if your site is content-heavy and each page is cheap to render, plain anchors give you clean caching semantics and zero client routing state, and Qwik is one of the few frameworks where that is not a performance regression.

import { component$ } from '@builder.io/qwik';
import { Link, useNavigate, useLocation } from '@builder.io/qwik-city';

export default component$(() => {
  const nav = useNavigate();
  const loc = useLocation();

  return (
    <>
      {loc.isNavigating && <span>Loading...</span>}
      <Link href='/jobs' prefetch>Browse jobs</Link>
      <p>Current slug: {loc.params.slug}</p>

      <button onClick$={async () => { await nav('/dashboard', { replaceState: true }); }}>
        Go to dashboard
      </button>
    </>
  );
});
Q13

What are the styling options in Qwik, and what is the difference between useStyles$ and useStylesScoped$?

BasicStyling

Answer

Qwik gives you four practical options and each has different lazy-loading behaviour. Plain global CSS imported in root.tsx is bundled and linked normally, which is right for resets and design tokens. CSS Modules work out of the box through Vite: import styles from './card.module.css' and use styles.card, with the class names hashed at build time and the CSS extracted into the client bundle. useStyles$ takes a QRL of a CSS string, usually written as useStyles$(inlineCss) with an import of a .css file using the ?inline suffix, and Qwik injects that stylesheet once per application when the component first renders, on the server if the component is server-rendered. useStylesScoped$ does the same but rewrites every selector with a generated data attribute so the rules cannot leak, and it enables the special :global() escape and Slot-aware selectors.

The reason both exist is resumability: Qwik needs to know a component's styles are present in the HTML even though the component's JavaScript may never load, so styles are collected during SSR rather than injected by client-side code the way CSS-in-JS libraries do. That also explains why runtime CSS-in-JS libraries are a bad fit here, they assume a hydration pass that Qwik does not run. Tailwind is the most common choice on real projects and installs cleanly with npm run qwik add tailwind. For interviews, the sharpest point is that scoped styles cost you nothing at runtime because scoping happens at build and SSR time, not in the browser.

import { component$, useStylesScoped$, Slot } from '@builder.io/qwik';
import styles from './card.css?inline';

export const Card = component$(() => {
  useStylesScoped$(styles); // selectors rewritten with a data attribute
  return (
    <div class='card'>
      <Slot />
    </div>
  );
});

/* card.css
.card { border: 1px solid #e2e8f0; border-radius: 8px; }
:global(body.dark) .card { border-color: #334155; }
::slotted(p) { margin: 0; }
*/
Q14

How does content projection work with the Slot component, and what are its rules?

BasicComponents

Answer

Slot is Qwik's version of children, and it is closer to the Web Components slot element than to React's props.children. You place a Slot inside a component to mark where projected content goes, and you can name slots so a caller can target several positions: <Slot name='header' /> receives any child element carrying q:slot='header'. The reason Qwik uses a declarative slot instead of passing children as a prop is serialisation.

Projected content belongs to the parent component, so it must be able to render, resume and update independently of the child, and a function-shaped children prop would break that. Two rules follow and both are interview material. First, you cannot read or transform projected content inside the child: there is no children array to map over, filter or count, because the content is not a value the child owns.

If you need that, pass data as props and render it yourself. Second, projected content that is conditionally hidden is not destroyed. Qwik moves it into a q:template element in the DOM and moves it back when the condition flips, which preserves its state and its listeners, but it also means the nodes still exist in the document and your CSS selectors may match them if you are careless.

Slot content is also rendered in the parent's scope, so it sees the parent's context and signals, not the child's. Mention this last point if you are asked why a store from the child is not visible inside the projected markup.

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

export const Panel = component$(() => {
  const open = useSignal(true);
  return (
    <section>
      <header><Slot name='title' /></header>
      <button onClick$={() => (open.value = !open.value)}>Toggle</button>
      {open.value && (
        <div class='body'>
          <Slot />          {/* default slot, kept alive in q:template when hidden */}
        </div>
      )}
    </section>
  );
});

export const Page = component$(() => (
  <Panel>
    <h2 q:slot='title'>Applications</h2>
    <p>Projected content renders in the parent scope.</p>
  </Panel>
));
Q15

When should you reach for useComputed$ instead of useTask$, and what are the constraints on the computed function?

IntermediateState

Answer

useComputed$ derives a read-only signal from other signals and stores. It is lazy and pull-based: the function does not run until something reads .value, and it re-runs only when a dependency it read during the last evaluation changes. Dependencies are collected automatically, so unlike useTask$ there is no track() call.

The constraints are strict and they show up in interviews. The function must be synchronous, must be pure, and must not have side effects, because Qwik may evaluate it during SSR, discard the result, and evaluate it again on the client. If you need async work, that is useResource$ or routeLoader$, not a computed.

If you need side effects such as writing to another signal, calling an API, or touching localStorage, that is useTask$ or useVisibleTask$. The practical rule is: derived values that render into JSX belong in useComputed$, and reactions to state changes belong in useTask$. Getting this wrong is the most common intermediate mistake.

People write a useTask$ that tracks a signal and writes the derived result into a second signal, which works but adds a second serialised value to the HTML, an extra subscription, and a re-render ordering hazard. The computed version serialises nothing extra because Qwik can recompute it on demand. There is also a sync variant, useComputed$ with a trivially serialisable expression, which the optimizer can keep small; if your computed pulls in a heavy library, that library gets loaded whenever the value is first read, so keep formatting helpers light or move them to the server.

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

export const Cart = component$(() => {
  const items = useStore([{ price: 499, qty: 2 }, { price: 1299, qty: 1 }], { deep: true });
  const coupon = useSignal(10);

  // Derived, lazy, no extra serialised state
  const subtotal = useComputed$(() => items.reduce((s, i) => s + i.price * i.qty, 0));
  const payable = useComputed$(() => Math.round(subtotal.value * (1 - coupon.value / 100)));

  return (
    <p>
      Subtotal โ‚น{subtotal.value}, payable โ‚น{payable.value}
    </p>
  );
});

// Anti-pattern: a task that mirrors derived state into another signal
// useTask$(({ track }) => { track(() => coupon.value); payable.value = ...; });

Key Points

  • useComputed$ is lazy, auto-tracking and read-only
  • The function must be synchronous and pure
  • Async work belongs in useResource$ or routeLoader$
  • Side effects belong in useTask$ or useVisibleTask$
  • Computed values are recomputed, not serialised twice
Q16

Explain Qwik's serialisation boundary. What triggers 'Only primitive and object literals can be serialized'?

IntermediateSerialization

Answer

Every dollar boundary is a point where the runtime may have to write the closure's captured variables into the HTML and read them back in a different JavaScript context. That is only possible for values Qwik knows how to serialise: primitives, plain objects and arrays, Date, URL, RegExp, Map, Set, Error, typed arrays, signals, stores, QRLs and references to DOM elements captured through ref. Anything else fails.

The classic trigger is capturing a value whose prototype carries behaviour: a class instance with methods, a database or Redis client, a WebSocket, a third-party SDK object, a plain function that was not wrapped in $(). At runtime that throws the QWIK Code(3) error, 'Only primitive and object literals can be serialized', and at build time the eslint rule qwik/valid-lexical-scope usually catches it first with a message naming the identifier. The second, less obvious rule is that captured bindings must be const.

A let can be reassigned after the closure is created, and since the closure is serialised rather than kept in memory, Qwik cannot observe that reassignment, so the lint rule rejects it outright. The fix depends on intent: if the value should change over time, put it in a signal and capture the signal; if the value is a helper function, wrap it in $() so it becomes a QRL; if it is a server-only resource such as a Prisma client, do not capture it at all, move the work into routeLoader$, routeAction$ or server$ where it stays on the server. Interviewers often paste a broken handler and ask you to fix it, so practise recognising the three fixes on sight.

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

export const Broken = component$(() => {
  let count = 0;                       // let: rejected by qwik/valid-lexical-scope
  const formatter = (n: number) => n;  // plain function: not serialisable

  return (
    <button onClick$={() => {
      count++;                         // QWIK Code(3) at runtime
      db.user.count();                 // server client captured into the browser
      formatter(count);
    }}>Broken</button>
  );
});

export const Fixed = component$(() => {
  const count = useSignal(0);                       // const + signal
  const format = $((n: number) => n.toLocaleString('en-IN')); // QRL

  return <button onClick$={async () => { count.value++; console.log(await format(count.value)); }}>
    Fixed
  </button>;
});
๐Ÿ’ก Pro Tip: Keep eslint-plugin-qwik enabled in CI. Almost every serialisation error is caught statically, and the runtime message alone rarely tells you which line captured the offending value.
Q17

What does noSerialize() do, and what happens to that value after the page resumes?

IntermediateSerialization

Answer

noSerialize() marks a value as deliberately excluded from serialisation. You use it when you must keep a non-serialisable object in Qwik state, typically a browser-only client: a Chart.js instance, a Mapbox map, a Firebase or Socket.IO connection, an AbortController, a MediaRecorder. Wrapping it lets you store it in a signal or store without triggering the Code(3) error, and the TypeScript type becomes NoSerialize<T>, which is T | undefined.

That undefined is the entire point and the thing candidates forget. After SSR, or after a page is paused and resumed, the value is gone. Any code that reads it must handle undefined and, if it needs the object, recreate it.

The correct pattern is to create such objects inside useVisibleTask$, store them with noSerialize, and re-create them in that same task if the value is undefined when the task runs. Two production consequences follow. First, never put application data behind noSerialize just to silence an error, because that data will silently vanish on resume and your bug will only reproduce on a cold load from real HTML, not in a client-side navigation during development.

Second, always pair the creation with a cleanup callback that disposes the underlying resource, otherwise a client-side navigation leaks the socket or the observer. If an interviewer asks how you would integrate a heavy imperative library such as a video player into Qwik, the answer they want is exactly this trio: dynamic import inside useVisibleTask$, noSerialize for the handle, cleanup on teardown.

import { component$, useSignal, useVisibleTask$, noSerialize, type NoSerialize } from '@builder.io/qwik';
import type { Socket } from 'socket.io-client';

export const LiveFeed = component$(() => {
  const socket = useSignal<NoSerialize<Socket>>();  // type is Socket | undefined
  const messages = useSignal<string[]>([]);

  // eslint-disable-next-line qwik/no-use-visible-task
  useVisibleTask$(async ({ cleanup }) => {
    if (!socket.value) {
      const { io } = await import('socket.io-client');
      const s = io('/feed');
      s.on('msg', (m: string) => (messages.value = [...messages.value, m]));
      socket.value = noSerialize(s);   // never written into the HTML
    }
    cleanup(() => socket.value?.disconnect());
  });

  return <ul>{messages.value.map((m) => <li key={m}>{m}</li>)}</ul>;
});
Q18

How do routeAction$, zod$ and the Form component work together, and why do they still work with JavaScript disabled?

IntermediateData Loading

Answer

routeAction$ declares a server-side mutation bound to a route. Like a loader, it is exported from a route file with a use-prefixed name; unlike a loader, it runs only in response to a POST. Wrapping it with zod$ attaches a Zod schema, and Qwik validates the submitted FormData against that schema before your handler body runs, giving you typed data and populating action.value.fieldErrors when validation fails.

In the component you call the hook and pass the returned object to the Form component, which renders a real HTML form with method POST and the correct action URL. That is why the flow degrades gracefully: with JavaScript disabled the browser performs a native form submission, the server runs the action and re-renders the page with the result. With JavaScript available, Qwik intercepts the submit, posts the data, and patches the DOM without a reload.

The action object exposes isRunning for pending UI, value for the returned result or validation failure, and submit() for programmatic submission. Return fail(400, { message }) from the handler for expected errors so the client gets a typed failure instead of an exception, and throw redirect(302, '/thanks') for the post-redirect-get pattern. globalAction$ is the same primitive without route binding: define it anywhere and use it from any component, which suits things like a newsletter signup in the footer. One important production behaviour: Qwik Router validates the Origin header on action POSTs and rejects mismatches with a CSRF error, so if you front the app with a proxy that rewrites Host or Origin you will see submissions fail in production but not locally.

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

export const useApply = routeAction$(
  async (data, { redirect, env }) => {
    const saved = await saveLead(data, env.get('CRM_KEY'));
    if (!saved) return { failed: true, message: 'Could not save, try again' };
    throw redirect(302, '/apply/thanks');
  },
  zod$({
    name: z.string().min(2),
    phone: z.string().length(10),
    ctc: z.coerce.number().min(0),
  })
);

export default component$(() => {
  const apply = useApply();
  return (
    <Form action={apply}>
      <input name='name' />
      {apply.value?.fieldErrors?.name && <span>{apply.value.fieldErrors.name}</span>}
      <input name='phone' />
      <input name='ctc' type='number' />
      <button disabled={apply.isRunning}>{apply.isRunning ? 'Saving...' : 'Apply'}</button>
    </Form>
  );
});
Q19

What does server$ give you that routeLoader$ does not, and what are its security implications?

IntermediateData Loading

Answer

server$ turns any function into a remote procedure you can call from the browser on demand. A loader runs when a route is entered; a server function runs whenever you call it, which makes it right for lazy interactions: an autocomplete lookup, a like button, polling a job status, an LLM completion. The optimizer registers the function as an endpoint under /_server/<hash>, the client call serialises the arguments as JSON in a POST, and the return value is deserialised back.

Written with the function keyword rather than an arrow, this inside the body is the RequestEvent, so you get cookie, env, request, headers, sharedMap and url without threading them through parameters. Declared as an async generator, it streams: every yield is flushed to the caller as it happens, which is how people build token-by-token AI responses in Qwik without a WebSocket. The security point is the one interviewers push on.

A server$ function is a public HTTP endpoint, not a private helper. Anyone can POST arbitrary arguments to it, so you must validate inputs and check authorisation inside the function body exactly as you would in an API route. Reading a session cookie through this.cookie and rejecting unauthenticated callers is not optional. Also keep payloads small, because arguments and return values cross the network as JSON on every call, and avoid calling server$ in a tight loop or per keystroke without debouncing, since each call is a round trip plus a serialisation cost.

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

// Plain RPC: note `function`, not an arrow, so `this` is the RequestEvent
export const searchCities = server$(async function (term: string) {
  const user = this.sharedMap.get('user');
  if (!user) throw this.error(401, 'Unauthorised');   // public endpoint, always check
  if (term.length < 2) return [];
  return db.city.findMany({ where: { name: { startsWith: term } }, take: 10 });
});

// Streaming RPC with an async generator
export const streamSummary = server$(async function* (jobId: string) {
  for await (const chunk of llm.stream(jobId)) yield chunk;
});

export const Search = component$(() => {
  const out = useSignal('');
  return (
    <button onClick$={async () => {
      for await (const chunk of await streamSummary('job-42')) out.value += chunk;
    }}>Summarise</button>
  );
});

Key Points

  • server$ is on-demand RPC; routeLoader$ is per-navigation data
  • Compiles to a real POST endpoint under /_server/
  • Use function syntax so `this` is the RequestEvent
  • Async generators stream results chunk by chunk
  • Treat it as public: validate arguments and check auth inside
Q20

When would you use useResource$ with the Resource component instead of routeLoader$?

IntermediateData Loading

Answer

routeLoader$ is tied to navigation: it runs once per request for the matched route and its value only changes when you navigate or call a refresh. useResource$ is tied to state: it re-runs whenever a tracked signal changes, which makes it the right tool for data that depends on in-page interaction, such as a filter dropdown, a paginated table, or a dependent select where the second list depends on the first. The hook returns a ResourceReturn that the Resource component consumes, and Resource takes onPending, onRejected and onResolved renderers so loading and error states are declarative rather than a pile of boolean signals. During SSR the resource is awaited so the resolved markup is in the initial HTML, which keeps it SEO-safe; after resumption, changes to tracked values re-run the fetch in the browser.

Two details separate good answers from vague ones. First, the resource function receives track and cleanup: track establishes the dependency, and cleanup gives you an AbortController hookup so a superseded request is actually cancelled rather than racing the new one. Second, useResource$ runs in the browser after the first pass, so it cannot touch server secrets.

If the query needs a private API key or a database, call a server$ function from inside the resource, or keep it in a loader. In practice most production Qwik pages use both: a loader for the initial page payload and a resource for whatever the user filters after arriving.

import { component$, useSignal, useResource$, Resource } from '@builder.io/qwik';
import { server$ } from '@builder.io/qwik-city';

const fetchJobs = server$(async (city: string) => db.job.findMany({ where: { city } }));

export default component$(() => {
  const city = useSignal('Bengaluru');

  const jobs = useResource$<Job[]>(async ({ track, cleanup }) => {
    track(() => city.value);
    const ctrl = new AbortController();
    cleanup(() => ctrl.abort());          // cancel the superseded request
    return fetchJobs(city.value);
  });

  return (
    <>
      <select onChange$={(_, el) => (city.value = el.value)}>
        <option>Bengaluru</option>
        <option>Pune</option>
      </select>
      <Resource
        value={jobs}
        onPending={() => <p>Loading jobs...</p>}
        onRejected={(e) => <p>Failed: {e.message}</p>}
        onResolved={(list) => <ul>{list.map((j) => <li key={j.id}>{j.title}</li>)}</ul>}
      />
    </>
  );
});
Q21

How does Qwik's context API work, and what must be true of the values you put in it?

IntermediateState

Answer

Qwik context avoids prop drilling the same way React context does, but the API is split into three pieces for serialisation reasons. createContextId<T>('app.theme') creates a typed, uniquely named handle and must be called at module scope, never inside a component, because the string id is what links the provider to the consumer across the resumability boundary. useContextProvider(ThemeContext, value) is called in a component's setup body and makes the value available to that component's entire subtree. useContext(ThemeContext) reads it from any descendant, and it throws if no provider exists above, so wrap the read in a try or provide a default at the root. The value you provide must be serialisable, and in practice you almost always provide a store or a signal rather than a plain object, because that is what makes updates propagate: a consumer deep in the tree that reads store.theme subscribes to exactly that property, and setting it from anywhere patches only the DOM that depends on it, with no component re-render anywhere in between. Contexts are per-container, so an embedded Qwik container on the same page has its own context tree.

Two failure modes worth naming: providing a plain object instead of a store, which gives you a value that never updates, and calling useContextProvider conditionally, which the qwik/use-method-usage lint rule rejects because setup calls must be unconditional and in a stable order. Context is also the standard way to share auth or theme data set by a root layout loader with deeply nested components without re-fetching.

import { component$, createContextId, useContextProvider, useContext, useStore, Slot, type Signal } from '@builder.io/qwik';

interface Session { userId: string | null; plan: 'free' | 'gold' }

// Module scope, stable string id
export const SessionCtx = createContextId<Session>('app.session');

export const Shell = component$(() => {
  const session = useStore<Session>({ userId: null, plan: 'free' });
  useContextProvider(SessionCtx, session);   // must be unconditional
  return <Slot />;
});

export const UpgradeBanner = component$(() => {
  const session = useContext(SessionCtx);
  if (session.plan === 'gold') return null;
  return <button onClick$={() => (session.plan = 'gold')}>Upgrade</button>;
});
Q22

How does Qwik prefetch code, and how do PrefetchServiceWorker, PrefetchGraph and prefetchStrategy fit together?

IntermediatePerformance

Answer

Resumability moves work from load time to interaction time, so prefetching is what stops the first click from feeling slow. The mechanism has three parts. During SSR, Qwik knows which symbols the current page can reach and emits a prefetch instruction set, controlled by the prefetchStrategy option passed to renderToStream in entry.ssr.tsx; symbolsToPrefetch defaults to using the manifest to select likely symbols.

In root.tsx you render PrefetchServiceWorker, which installs a service worker whose job is to fetch and cache those chunks into the browser cache in the background, at low priority, so they are already local when a handler is needed. PrefetchGraph loads the bundle graph, a build artefact describing which chunks depend on which, so the service worker can pull a symbol together with its transitive dependencies in one pass rather than discovering them one round trip at a time. Together these mean a click typically resolves from cache rather than the network.

Things to say in an interview: prefetching is a background download, so it competes for bandwidth on the 3G and congested 4G connections common outside Indian metros, and tuning which symbols get prefetched matters more there than on a fibre connection. Link supports prefetching the next route's data and code on hover or on viewport entry. And you should verify the behaviour in the network panel of a production preview build, filtered to the service worker, because dev mode does not install it. If the service worker is blocked, for example on a non-HTTPS origin or by a strict CSP, Qwik still works but the first interaction pays a real network fetch.

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

export default component$(() => (
  <QwikCityProvider>
    <head>
      <PrefetchServiceWorker />
      <PrefetchGraph />
    </head>
    <body>
      <RouterOutlet />
    </body>
  </QwikCityProvider>
));

// src/entry.ssr.tsx
export default function (opts: RenderToStreamOptions) {
  return renderToStream(<Root />, {
    manifest,
    prefetchStrategy: {
      implementation: { linkInsert: null, workerFetchInsert: null, prefetchEvent: 'always' },
    },
    ...opts,
  });
}
๐Ÿ’ก Pro Tip: Measure prefetch behaviour on a throttled Slow 4G profile, not on office wifi. The whole point of the feature only shows up when bandwidth is scarce.
Q23

How do you call event.preventDefault() in Qwik when the handler has not downloaded yet?

IntermediateEvents

Answer

This is the sharpest consequence of resumability. A normal onClick$ handler is a QRL, so it resolves asynchronously; by the time the chunk arrives the browser has already processed the default action, which means calling preventDefault inside it does nothing for a link navigation or a form submit. Qwik offers two answers.

The declarative one is the preventdefault:<event> attribute, which the framework reads during SSR and applies synchronously in qwikloader before dispatching to your handler: preventdefault:click on an anchor stops the navigation regardless of when the handler loads. There is a matching stoppropagation:<event>. The programmatic one, available in recent 1.x versions, is sync$().

It wraps a function whose source is serialised into the HTML and executed synchronously by qwikloader, before the asynchronous QRL runs. The constraint is severe and interviewers will check you know it: a sync$ function cannot capture any lexical scope at all, no signals, no props, no imports, because there is no closure to restore, only stringified source. It receives the event and the element and must work from those alone.

You compose the two by passing an array to the handler prop: the sync part first, the async part second. Use this for preventing default, stopping propagation, or setting an immediate visual state such as toggling a class; keep everything else in the async handler. If you find yourself wanting real state in a sync handler, that is a sign the interaction should be handled by CSS or by a native form instead.

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

export const Confirm = component$(() => {
  const clicks = useSignal(0);

  return (
    <>
      {/* Declarative: applied by qwikloader before the QRL resolves */}
      <a href='/delete' preventdefault:click onClick$={() => clicks.value++}>
        Delete (declarative)
      </a>

      {/* Programmatic: sync part runs first, cannot capture anything */}
      <a
        href='/delete'
        onClick$={[
          sync$((e: MouseEvent) => {
            e.preventDefault();
            e.stopPropagation();
          }),
          $(() => {
            clicks.value++;   // async part, capture is allowed here
          }),
        ]}
      >
        Delete (sync$)
      </a>
    </>
  );
});

Key Points

  • QRL handlers are async, so preventDefault inside them is too late
  • preventdefault:click and stoppropagation:click are handled by qwikloader
  • sync$ source is serialised into HTML and runs synchronously
  • sync$ cannot capture any lexical scope, signals or imports
  • Pass an array of handlers to combine sync$ and $ on one event
Q24

How does qwikify$ let you reuse React components, and what does each island actually cost?

IntermediateInteroperability

Answer

Run npm run qwik add react and Qwik scaffolds an integrations folder plus the build wiring for @builder.io/qwik-react. You then write the React component in a file whose first line is the JSX pragma comment pointing at react, and export a wrapped version with qwikify$(Component, { eagerness }). The wrapper renders the React tree to HTML during SSR and, when the chosen trigger fires, boots a real React root over that markup.

Triggers are set per usage with client: props: client:load, client:idle, client:visible, client:hover, client:signal={sig} and client:only, which skips SSR entirely for components that cannot render on the server. The cost is what interviewers want to hear. Every island that activates downloads React and ReactDOM plus the component's own code, so the first qwikified island on a page typically costs tens of kilobytes of JavaScript and a synchronous hydration of that subtree.

Islands are independent React roots, which means they do not share React context or state with each other; the supported way to coordinate them is Qwik signals passed as props, since a signal read inside a qwikified component makes it re-render when the signal changes. Props must be serialisable like any Qwik props, and DOM events on the host use the host: prefix, for example host:onClick$. The correct framing for a design question is that qwikify$ is a migration and escape-hatch tool: use it for a date picker or a rich text editor that would take weeks to port, not as a way to keep writing React inside a Qwik shell, because at that point you have all of React's runtime cost plus Qwik's constraints.

/** @jsxImportSource react */
import { qwikify$ } from '@builder.io/qwik-react';
import { DateRangePicker } from 'some-react-datepicker';

// One island per wrapped component
export const QDatePicker = qwikify$(DateRangePicker, { eagerness: 'hover' });

// ---- usage in a Qwik component ----
import { component$, useSignal } from '@builder.io/qwik';
import { QDatePicker } from './date-picker';

export default component$(() => {
  const range = useSignal({ from: '', to: '' });
  return (
    <QDatePicker
      client:visible               // activate only when scrolled into view
      value={range.value}
      host:onClick$={() => console.log('island clicked')}
    />
  );
});
๐Ÿ’ก Pro Tip: Count your islands before shipping. Three qwikified React widgets on a landing page can undo every byte resumability saved you.
Q25

How do you test a Qwik application, and why do unit tests miss serialisation bugs?

IntermediateTesting

Answer

Qwik ships a testing utility, createDOM() from @builder.io/qwik/testing, that renders a component into a simulated document and returns render, screen and userEvent helpers. Paired with Vitest, which the starter can configure through npm run qwik add vitest, this gives fast component tests: render the component, query the DOM, dispatch an event through userEvent('button', 'click'), assert on the resulting markup. It is the right tool for pure rendering logic, conditional branches and computed values.

The important limitation, and the answer interviewers are listening for, is that createDOM runs your components in a single JavaScript context without the optimizer, without SSR serialisation and without a pause and resume cycle. That means a handler capturing a non-serialisable value, a let binding, or a Prisma client will pass a unit test happily and then fail as a Code(3) error only when a real user loads real server-rendered HTML. Serialisation and resumability are integration properties, so they need an end-to-end test: npm run qwik add playwright, run the tests against a production preview build rather than the dev server, and assert on behaviour after a cold load.

A resumability-specific assertion worth writing is a check that no application chunk is requested before the first interaction, done by listening to page requests in Playwright and then clicking. Server code gets covered separately: routeLoader$ and server$ functions are ordinary async functions, so extract the logic into a plain module and unit test that, keeping the Qwik wrapper thin.

// counter.spec.tsx (Vitest)
import { createDOM } from '@builder.io/qwik/testing';
import { test, expect } from 'vitest';
import { Counter } from './counter';

test('increments on click', async () => {
  const { screen, render, userEvent } = await createDOM();
  await render(<Counter />);
  expect(screen.querySelector('button')?.textContent).toBe('0');
  await userEvent('button', 'click');
  expect(screen.querySelector('button')?.textContent).toBe('1');
});

// e2e/resumability.spec.ts (Playwright, against `npm run preview`)
import { test, expect } from '@playwright/test';

test('ships no app JS until the first click', async ({ page }) => {
  const chunks: string[] = [];
  page.on('request', (r) => { if (r.url().includes('/build/q-')) chunks.push(r.url()); });
  await page.goto('/');
  await page.waitForLoadState('networkidle');
  const beforeClick = chunks.length;
  await page.getByRole('button', { name: 'Apply' }).click();
  expect(chunks.length).toBeGreaterThan(beforeClick);
});
Q26

What are useOn, useOnDocument and useOnWindow for, and how are they cheaper than useVisibleTask$?

IntermediateEvents

Answer

These hooks attach an event listener programmatically instead of through JSX. useOn binds to the component's host element, useOnDocument binds to the document, and useOnWindow binds to window. Each takes an event name and a QRL, and Qwik records the binding as an attribute during SSR exactly as it would for an inline onClick$, which is the key property: nothing downloads until the event actually fires. Compare that with useVisibleTask$, which eagerly downloads and executes a chunk as soon as the component becomes visible or the page idles.

If all you wanted was to react to a scroll, a resize, a keydown or the browser's online and offline events, the useOn family gives you the same capability at zero startup cost. They are also the only way to attach listeners from a reusable custom hook, because a hook has no JSX of its own to hang attributes on, which makes them the backbone of shared behaviour libraries in Qwik. Qwik's own lifecycle events are available through the same mechanism: useOnDocument('qinit', ...) fires once qwikloader has initialised the container, and 'qidle' fires when the browser goes idle, which is a lighter way to schedule non-urgent work than a visible task with idle eagerness. Two practical notes: document and window listeners are not automatically removed when the component unmounts in every case, so avoid attaching heavy handlers in components that mount and unmount frequently, and remember that the handler is still a QRL, so the first event pays a fetch unless the chunk was prefetched.

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

// Reusable hook: no JSX, so useOn* is the only way to bind listeners
export function useOnlineStatus() {
  const online = useSignal(true);
  useOnWindow('offline', $(() => (online.value = false)));
  useOnWindow('online', $(() => (online.value = true)));
  return online;
}

export const StatusBar = component$(() => {
  const online = useOnlineStatus();

  useOn('mouseenter', $(() => console.log('host hovered')));
  useOnDocument('qinit', $(() => performance.mark('qwik-resumed')));

  return <div>{online.value ? 'Online' : 'You are offline'}</div>;
});
Q27

How does middleware work in Qwik Router, and what do plugin@ files, sharedMap and cacheControl do?

IntermediateServer and Middleware

Answer

Every route file and layout can export request handlers: onRequest runs for all methods, and onGet, onPost, onPut, onDelete run for their verb. They receive a RequestEvent and run top down, outermost layout first, awaiting next() to continue the chain, which gives you a familiar middleware pipeline without a separate server framework. Files named plugin@something.ts placed directly in src/routes are global middleware that run before any route handler, in alphabetical order of the name after the at-sign, which is where auth, logging and rate limiting normally live.

RequestEvent is the whole server API surface: params, url, request, cookie for reading and setting cookies with proper attributes, env.get for environment variables, headers for response headers, json/text/send for returning a response directly, error and redirect for throwing typed responses, and platform for runtime specific bindings such as Cloudflare KV or D1. sharedMap is a per-request Map used to pass data down the chain, so an auth plugin resolves the session once and every loader and action below reads sharedMap.get('user') instead of re-verifying the token. cacheControl sets caching headers declaratively and is where a lot of real Qwik performance comes from: a public page with maxAge 5 and staleWhileRevalidate of a week serves instantly from a CDN while revalidating in the background. Two gotchas to mention: middleware runs on client-side navigations too, because the router fetches q-data.json through the same pipeline, and throwing redirect inside a plugin is the correct way to gate routes, since returning early without throwing continues the chain.

// src/routes/plugin@auth.ts (runs before every route)
import type { RequestHandler } from '@builder.io/qwik-city';

export const onRequest: RequestHandler = async ({ cookie, sharedMap, next, redirect, url, cacheControl }) => {
  cacheControl({ public: true, maxAge: 5, staleWhileRevalidate: 60 * 60 * 24 * 7 });

  const token = cookie.get('session')?.value;
  const user = token ? await verify(token) : null;
  sharedMap.set('user', user);            // available to every loader and action

  if (!user && url.pathname.startsWith('/dashboard')) {
    throw redirect(302, `/login?next=${url.pathname}`);
  }
  await next();
};

// Any loader below can now do:
// export const useMe = routeLoader$(({ sharedMap }) => sharedMap.get('user'));
Q28

How do deployment adapters work, and what changes between the Node, Cloudflare Pages and static adapters?

IntermediateDeployment

Answer

A Qwik Router app is platform agnostic at the source level; the adapter supplies the glue between the platform's request object and Qwik's handler. Running npm run qwik add cloudflare-pages, vercel-edge, netlify-edge, express, bun, deno or static adds two things: a vite config under adapters/<name>/vite.config.ts and a platform entry under src/entry.<name>.tsx, then rewires the build script so npm run build produces both the client bundles and the platform-specific server output. The Node and Express adapter builds a server bundle you run with node server/entry.express.js behind PM2 or a container, and you get the full Node API surface.

The edge adapters build a worker bundle instead, which changes what your server code may use: no fs, no native modules, no long-lived connections, and any database driver must be HTTP based or a platform binding, which is exactly why teams get surprised when Prisma or a raw pg client fails only in production. On Cloudflare, platform.env exposes KV, D1 and R2 bindings through RequestEvent, and local development needs wrangler to inject them. The static adapter is different in kind: it crawls your routes at build time and writes HTML files, giving you SSG with no server at all, configured with the origin and the list of routes to include or exclude.

You can mix modes, prerendering marketing pages while keeping the dashboard server-rendered. For Indian traffic the practical note is edge placement: Cloudflare and Vercel both serve from Mumbai and Chennai points of presence, which usually beats a single Node instance in one region for TTFB.

# Pick a target; each command patches package.json and adds adapter files
npm run qwik add cloudflare-pages   # -> adapters/cloudflare-pages/vite.config.ts
npm run qwik add express            # -> src/entry.express.tsx, node server bundle
npm run qwik add static             # -> SSG, writes HTML at build time

# adapters/static/vite.config.ts
export default extendConfig(baseConfig, () => ({
  build: { ssr: true, rollupOptions: { input: ['src/entry.ssr.tsx', '@qwik-city-plan'] } },
  plugins: [
    staticAdapter({
      origin: 'https://goodspace.ai',
      filter: (pathname) => !pathname.startsWith('/dashboard'),
    }),
  ],
}));

// Accessing a Cloudflare binding inside a loader
// export const useFlag = routeLoader$(async ({ platform }) => platform.env.FLAGS.get('beta'));
Q29

What exactly does Qwik write into the HTML when it serialises state, and how does that payload become a performance problem?

AdvancedSerialization

Answer

At the end of SSR, Qwik walks everything reachable from the rendered tree and writes a state block, a script of type qwik/json in 1.x, into the container. That block holds three things: an array of objects, deduplicated so a value referenced from five places is stored once and referenced by index; the subscription graph, which records that object N feeds DOM node M or task T; and the captured lexical scopes for every QRL, stored as index lists that appear in the attributes, which is what the trailing [0 3] in an on:click value means. Element references are stored as pointers to q:id attributes rather than as copies, so a signal holding a DOM node round-trips correctly.

Deserialisation is lazy and partial: touching one handler reconstructs only the objects that handler's index list points at, which is the mechanism behind constant-time startup. The failure mode is payload growth. Anything reachable gets serialised, so a loader that returns a full API response, a store used as a client-side cache, or a large list held in state all end up as bytes in the document.

That hurts twice: the HTML is bigger over the wire, and the browser parses a large JSON blob during load. Real symptoms are a Largest Contentful Paint that regresses as data volume grows and a document that gzips poorly. The fixes are the ones a senior candidate should list unprompted: project loader results down to rendered fields, keep pagination server side, avoid storing derived data that useComputed$ can recreate, use noSerialize for anything the client can rebuild, and measure by comparing document transfer size against the same page with an empty dataset.

Key Points

  • State, subscription graph and captured scopes go into a qwik/json block
  • Objects are deduplicated and referenced by index from attributes
  • Deserialisation is lazy: only the touched closure's objects are revived
  • Everything reachable is serialised, so state size becomes document size
  • Project loader output, paginate, and prefer computed over stored values
๐Ÿ’ก Pro Tip: A quick production check: load the page, view source, and look at the size of the state script. If it is larger than your visible content, your state design is the bottleneck, not Qwik.
Q30

A user has your Qwik page open, you redeploy, and their next click does nothing. What happened and how do you prevent it?

AdvancedProduction

Answer

The HTML that user is looking at contains hard references to chunk file names generated by the previous build, because on:click attributes carry chunk#symbol pairs resolved against q:base. Symbol names and chunk hashes are content derived, so a new build renames them. If your deploy replaces the build directory, those old chunk URLs now 404, the dynamic import rejects, and the click silently does nothing.

This is the single most Qwik-specific production incident and interviewers at performance-minded teams love it because it tests whether you have actually run Qwik in production. The prevention is asset retention: deploy new builds alongside the old ones rather than replacing them, keep at least the previous few builds available at their original paths, and serve them from immutable, long-cached CDN objects. Platforms with atomic deploys and versioned asset retention, such as Cloudflare Pages, Vercel and Netlify, handle much of this for you; a plain nginx or S3 sync with a delete flag does not, and that is where teams get hurt.

Complementary measures matter too. Keep q:manifest-hash in the HTML and have the client detect a mismatch after a navigation so you can prompt a reload. Add a global handler for failed dynamic imports that reloads the page once rather than leaving the user with an inert UI.

If you run blue and green environments, ensure sticky routing so a resumed page keeps talking to the origin that served it. Finally, watch for the same class of bug with the service worker cache holding chunk lists from a stale bundle graph, and version the service worker with each deploy.

# Wrong: wipes the chunks that already-open pages reference
aws s3 sync dist/ s3://cdn-bucket/build/ --delete

# Right: immutable, versioned, additive
aws s3 sync dist/ s3://cdn-bucket/build/${BUILD_ID}/ \
  --cache-control 'public,max-age=31536000,immutable'
# and set q:base per deploy so the HTML points at its own build

// entry.ssr.tsx
export default function (opts: RenderToStreamOptions) {
  return renderToStream(<Root />, {
    manifest,
    base: `/build/${process.env.BUILD_ID}/`,   // becomes q:base in the HTML
    ...opts,
  });
}

// Optional safety net in root.tsx
// useOnWindow('unhandledrejection', $((e) => {
//   if (String(e.reason).includes('Failed to fetch dynamically imported module')) location.reload();
// }));
Q31

Users on a 3G connection say the first click takes two seconds. How do you diagnose and fix interaction latency in Qwik?

AdvancedPerformance

Answer

Start by naming the waterfall, because the fix depends on which segment is slow. A first interaction costs: qwikloader dispatch (microseconds), resolving the QRL attribute, a network fetch for the chunk if it is not already cached, fetching that chunk's transitive dependencies, deserialising the captured state, then running the handler. On a fast connection the fetch disappears into noise; on a congested mobile network in a tier-2 city it dominates.

Open a production preview build in Chrome with Slow 4G and CPU throttling, click, and read the network panel: if the chunk request starts at click time rather than being served from the service worker cache, prefetching is not doing its job. Common causes are a missing PrefetchServiceWorker in root.tsx, a service worker that cannot install because the origin is not HTTPS or a CSP blocks it, a missing PrefetchGraph so dependencies are discovered one round trip at a time, or a q:base pointing at a CDN with cold cache and no immutable caching headers. Second, look at the chunk itself.

A handler that pulls in a heavy library, a date formatting package or an analytics SDK, drags that whole dependency into the interaction path; move it to the server through server$ or load it behind an explicit user action. Third, measure the right metric. Interaction to Next Paint is what Core Web Vitals grades, so instrument it with the web-vitals library and report from real users rather than trusting a lab run. Finally, for genuinely latency-critical controls, do the immediate visual feedback with sync$ or CSS so the UI responds before the chunk lands.

// src/routes/layout.tsx: report real-user INP and the Qwik symbol timeline
import { component$, useOnDocument, Slot, $ } from '@builder.io/qwik';

export default component$(() => {
  useOnDocument('qinit', $(async () => {
    const { onINP, onLCP } = await import('web-vitals');
    const send = (m: { name: string; value: number }) =>
      navigator.sendBeacon('/api/vitals', JSON.stringify(m));
    onINP(send);
    onLCP(send);
  }));

  // qsymbol fires whenever Qwik resolves a symbol: use it to time chunk fetches
  useOnDocument('qsymbol', $((e: CustomEvent) => {
    performance.measure(`qsymbol:${e.detail.symbol}`);
  }));

  return <Slot />;
});

Key Points

  • Interaction cost equals QRL resolve plus chunk fetch plus deserialise plus run
  • Verify the service worker actually installed and prefetched
  • PrefetchGraph avoids serial dependency discovery round trips
  • Keep heavy libraries out of handler chunks; push work to server$
  • Track real-user INP with web-vitals, not lab-only numbers
Q32

What changes in the Qwik 2 line, and how would you plan a migration from @builder.io/qwik?

AdvancedVersions and Migration

Answer

The headline change is the package move: the core leaves @builder.io/qwik for @qwik.dev/core and the meta-framework, previously Qwik City under @builder.io/qwik-city, becomes @qwik.dev/router. Names in the docs follow, so Qwik City and Qwik Router refer to the same thing at different points in the project's history and you should use both terms comfortably in an interview. Under the hood the work concentrates on serialisation and reactivity: a rewritten serialiser that produces a smaller and faster-to-parse payload than the single qwik/json blob, finer-grained signal handling so more updates patch text nodes directly instead of re-rendering a component subtree, and a smaller qwikloader.

The public component API is deliberately conservative, component$, useSignal, useStore, useTask$, useComputed$, useResource$ and the router hooks all keep their shape, which is why most migrations are dominated by import rewrites rather than rewrites of logic. Plan it the way you would any framework major. Pin your current version and get the app green on the latest 1.x first, since many deprecations are already flagged there.

Audit third-party Qwik packages, because ecosystem libraries lag a major by months and a single unported UI kit can block you. Move imports module by module with a codemod or a scripted find and replace, keep the eslint plugin on so lexical scope violations surface immediately, and run your Playwright suite against a preview build after each batch, because serialisation regressions do not show up in unit tests. For a production app with revenue attached, the sane posture in 2026 is to stay on the stable line until your dependencies have shipped compatible releases, and to do the migration on a branch with real end-to-end coverage rather than incrementally in main.

// Qwik 1.x imports
import { component$, useSignal } from '@builder.io/qwik';
import { routeLoader$, Form } from '@builder.io/qwik-city';

// Qwik 2 line: same APIs, new package names
import { component$, useSignal } from '@qwik.dev/core';
import { routeLoader$, Form } from '@qwik.dev/router';

// Migration order that keeps risk low:
// 1. npm run build && npx playwright test   (green baseline on latest 1.x)
// 2. audit ecosystem deps for a v2-compatible release
// 3. scripted import rewrite, one directory at a time
// 4. re-run e2e against `npm run preview`, not the dev server
๐Ÿ’ก Pro Tip: Interviewers rarely want version trivia. They want to hear a migration plan with a baseline, a dependency audit, and end-to-end coverage as the safety net.
Q33

How do you implement authentication and protected routes in Qwik Router without leaking anything to the client?

AdvancedSecurity

Answer

The pattern that survives review has three layers. First, a global middleware file, src/routes/plugin@auth.ts, reads the session cookie on every request, verifies it, and writes the resolved user into sharedMap. Cookies are set through the RequestEvent cookie API with httpOnly, secure, sameSite lax or strict, and an explicit path and maxAge; never store a session token in a signal or a store, because anything in Qwik state is serialised into the HTML and therefore readable by any script on the page.

Second, gate at the loader level rather than in the component. A layout loader that reads sharedMap and throws redirect(302, '/login') protects every route under it, runs on the server, and cannot be bypassed by a client-side navigation because the router fetches loader data through the same middleware chain. Rendering a component and hiding it with a conditional is not protection; the markup was already generated.

Third, guard the mutations. routeAction$ handlers and server$ functions are independently reachable HTTP endpoints, so each one re-checks authorisation from sharedMap or the cookie rather than trusting that the caller came from a protected page. Qwik Router validates the Origin header on action POSTs and rejects mismatches with a CSRF error; leave that on, and if a reverse proxy rewrites Origin, fix the proxy rather than disabling the check. For the login flow itself, prefer a routeAction$ with a zod$ schema so it works without JavaScript and returns typed field errors, and use the post-redirect-get pattern. The community option most teams reach for is the Auth.js integration added with npm run qwik add auth, which wires providers and session handling into the same middleware pipeline.

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

export const onRequest: RequestHandler = async ({ cookie, sharedMap, next }) => {
  const raw = cookie.get('sid')?.value;
  sharedMap.set('user', raw ? await verifySession(raw) : null);
  await next();
};

// src/routes/(app)/layout.tsx  -> protects every route beneath it
import { routeLoader$ } from '@builder.io/qwik-city';

export const useUser = routeLoader$(({ sharedMap, redirect, url }) => {
  const user = sharedMap.get('user');
  if (!user) throw redirect(302, `/login?next=${encodeURIComponent(url.pathname)}`);
  return { id: user.id, name: user.name };   // only what the UI renders
});

// Setting the cookie after a successful login action
// cookie.set('sid', token, { httpOnly: true, secure: true, sameSite: 'lax', path: '/', maxAge: 60 * 60 * 24 * 7 });
Q34

How would you embed Qwik into an existing application, and what makes multiple containers on one page possible?

AdvancedArchitecture

Answer

Qwik's unit of isolation is the container, marked by the q:container attribute together with q:base, q:version and its own serialised state block. Containers are self-describing and independently resumable, which means a page can host several of them, rendered by different builds, deployed at different times, and none of them needs the others to boot. That is a genuinely different micro-frontend story from the usual module federation setup, where each remote still hydrates its own framework runtime on load.

In practice you use this in two ways. The incremental adoption path: keep your existing Rails, Django, WordPress or PHP application rendering pages, and have it inject Qwik-rendered HTML fragments for specific widgets, each fragment being a container with its own q:base pointing at the Qwik build output. The user gets interactive widgets that cost roughly a kilobyte of shared loader, and you migrate page by page without a big-bang rewrite.

The composition path: separate teams own separate containers on the same page, each deploying independently, coordinating only through the DOM and custom events, because JavaScript state does not cross a container boundary. That isolation is also the main constraint to state in an interview: context, signals and stores are scoped to their container, so shared state has to travel through URL, cookies, custom events or a small shared store attached to window. Watch two operational details: every container needs its q:base to survive your CDN layout, and duplicated styles across containers need a shared stylesheet or you will ship the same CSS several times.

<!-- A legacy server-rendered page hosting two independent Qwik containers -->
<div class='legacy-page'>
  <div q:container='paused' q:base='https://cdn.example.com/search/build/' q:version='1.x'>
    <input on:input='q-4f1.js#s_search' placeholder='Search jobs' />
    <script type='qwik/json'>{"objs":[]}</script>
  </div>

  <div q:container='paused' q:base='https://cdn.example.com/cart/build/' q:version='1.x'>
    <button on:click='q-9c7.js#s_addToCart'>Add to cart</button>
    <script type='qwik/json'>{"objs":[]}</script>
  </div>
</div>

<!-- Cross-container communication: DOM events, not shared imports -->
<script>
  document.addEventListener('cart:add', (e) => console.log(e.detail));
</script>

Key Points

  • q:container plus q:base makes a fragment independently resumable
  • Multiple containers can come from different builds and deploys
  • State, context and signals do not cross container boundaries
  • Ideal for incremental adoption inside a legacy server-rendered app
  • Coordinate across containers with custom events or the URL
Q35

When would you argue against using Qwik, and how do you structure a large Qwik codebase so the performance benefit survives?

AdvancedArchitecture

Answer

Argue against Qwik when the workload is a heavily interactive application behind a login, where nearly every component becomes active within seconds of load: an IDE, a design tool, a trading terminal, a real-time dashboard. Resumability optimises the path from load to first interaction, and if your users interact with everything immediately you pay the chunk fetches anyway while accepting a smaller ecosystem, fewer component libraries and a much thinner hiring pool. Argue against it when the team is small and the roadmap is feature-heavy, because the serialisation model imposes real discipline that a React team absorbs as friction.

Argue for it when first-load performance is the product: content sites, marketplaces, job boards, e-commerce listings, anything where Core Web Vitals feed SEO and conversion, and especially where the median user is on a mid-range Android phone and a congested mobile network. To keep the benefit as the codebase grows: treat useVisibleTask$ as a budgeted resource and review every addition; keep loader payloads projected down to rendered fields so the serialised state does not balloon; prefer useComputed$ over stored derived state; put heavy dependencies behind server$ so they never enter a client chunk; use the ?jsx image import so images get width, height and srcset automatically rather than shipping a JavaScript image component; pick a build-time i18n approach such as qwik-speak instead of a runtime translation library; and cap React islands with an explicit list that requires justification to extend. Finally, put a performance budget in CI, assert on document size and on the number of chunks requested before first interaction, because those are the two numbers that silently regress.

Key Points

  • Poor fit for app-shell products where everything activates immediately
  • Strong fit where first load, SEO and mid-range Android users decide revenue
  • Budget useVisibleTask$ and React islands explicitly in review
  • Project loader payloads; serialised state is document weight
  • Enforce document size and pre-interaction chunk count in CI
๐Ÿ’ก Pro Tip: The most persuasive answer names a case where you would not choose Qwik. Interviewers read unqualified enthusiasm as inexperience.

Companies Hiring Qwik

Builder.io
Cloudflare
Netlify
Vercel
Storyblok
Bejamas
GeekyAnts

Salary Insights

Average in India
โ‚น7-22 LPA

Frequently Asked Questions

What salary can a Qwik developer expect in India in 2026?

Roughly โ‚น7-22 LPA, and the spread is wide because almost nobody is hired for Qwik alone. Companies hire a strong frontend engineer, usually React or Vue first, who can also own a Qwik surface, so your base band is set by your overall frontend seniority and Qwik adds a premium of maybe 10 to 20 percent because the supply is thin. The upper end tends to be product studios, headless commerce and performance consultancies where page speed is contractual, plus remote roles with platform and CMS companies that pay in a global band. Freelance and contract work around Core Web Vitals rescue projects often pays better per hour than a salaried role.

How long does it take to become interview-ready in Qwik if I already know React?

Two to four weeks of focused evenings for a solid React developer. The syntax transfers almost entirely, JSX, components, props, so the real work is unlearning hydration-era instincts: no useEffect reflex, no destructuring props or stores, explicit track() in tasks, and the serialisation rules around $ closures. Budget the first week for the core runtime and the second for Qwik Router with loaders, actions and server$. The fastest way to make it stick is to build and deploy one real site, then open the production HTML and read the on:click attributes and the state block until the resumability story is obvious rather than memorised.

Can a fresher get hired for a Qwik role, or is it only for experienced developers?

Freshers do get hired, but not for Qwik as such. Entry-level frontend hiring in India still runs on JavaScript, TypeScript, React and CSS fundamentals, and interviewers will test those first. Where Qwik helps a fresher is differentiation: a deployed Qwik project with a 95-plus Lighthouse score, a short write-up on what resumability changed, and a Playwright test proving no application JavaScript loads before the first click is a far more memorable portfolio than another React todo app. Experienced candidates are judged differently, they are expected to discuss serialisation payloads, prefetch tuning, deployment adapters and failure modes in production.

Is Qwik still worth learning in 2026 when React Server Components exist?

They solve overlapping but different problems. React Server Components reduce how much component code reaches the browser, but the interactive parts of a React page still hydrate, so startup cost still scales with interactivity. Qwik removes the hydration step entirely. Worth learning if you work on content-heavy or commerce surfaces where load performance is measured and paid for, or if you want the strongest available answer in a systems-design round about client-side performance. Not worth prioritising over TypeScript depth, React and browser fundamentals if you are early in your career, because those are what most Indian job descriptions actually filter on.

How does Qwik compare with Astro and Next.js for a real project?

Astro ships zero JavaScript by default and hydrates islands you mark, which is excellent for content sites but means each island pays a hydration cost proportional to its size. Qwik keeps the whole page interactive without hydrating anything, so it scales better when interactivity is spread across the page rather than concentrated in a few widgets. Next.js has by far the largest ecosystem, the deepest hiring pool in India and the most mature tooling, and that is a legitimate reason to choose it. A reasonable rule: Astro for documentation and marketing, Qwik when the whole page is interactive and load performance is a revenue metric, Next.js when team velocity and ecosystem breadth matter more than the last few points of Core Web Vitals.

Are there enough Qwik jobs in India, and how do I find them?

The absolute number is small compared with React, so treat Qwik as a specialisation layered on a mainstream stack rather than a job search category on its own. Openings cluster in three places: product and design studios that sell page speed, headless commerce and CMS integration teams, and remote roles with platform companies in the Jamstack ecosystem. Search for performance-focused frontend roles that mention Core Web Vitals, resumability, islands or Lighthouse budgets rather than filtering on the framework name. Contributing to Qwik or a Qwik ecosystem library is unusually effective here, because the community is small enough that maintainers notice and referrals follow.

Introduction

Qwik is the framework that treats shipped JavaScript as the thing to avoid. Created at Builder.io by Misko Hevery, who also built Angular, it replaces hydration with resumability: the server serialises application state, event wiring and the reactivity graph into the HTML itself, and the browser picks up exactly where the server stopped without ever re-executing component code. A page can contain a hundred components and still download roughly a kilobyte of JavaScript until the user actually clicks something. Every other framework asks how to make hydration cheaper. Qwik's answer is to delete the hydration step entirely, which is why a Qwik interview looks nothing like a React interview.

That difference shows up in the first ten minutes of a screening call. Interviewers check whether you read the dollar suffix as a lazy-loading boundary rather than a naming convention, whether you can describe what the Rust optimizer does to a component$ closure at build time, and whether you know why capturing a let variable or a database client inside onClick$ throws a serialisation error. Beyond the core runtime, expect Qwik Router topics: routeLoader$, routeAction$ with zod$, server$ RPC calls, middleware in plugin@ files, and the deployment adapters for Cloudflare Pages, Vercel Edge and Node. Senior rounds move to prefetch tuning, serialisation payload size, and what happens to an open browser tab after you redeploy.

Qwik hiring in India is still niche, and that works in your favour. The roles that exist sit in product studios, headless commerce teams and performance consultancies where Core Web Vitals are written into the contract, and they pay around โ‚น7-22 LPA because the talent pool is thin. Most listings ask for strong React or Vue plus one demonstrated Qwik project, not five years of Qwik. This guide works through 35 questions grouped by difficulty, with runnable code wherever prose alone would be vague. Get the basics until resumability feels obvious, then spend your remaining prep time on serialisation and prefetching, because that is where senior offers are decided.

Ready to practice Qwik interviews?

Don't just read, practice these Qwik questions live with an AI interviewer that asks follow-ups and scores your answers.

โœ“AI-powered practice
โœ“Instant feedback
โœ“Free to start
Start Free Mock Interview