Lazy-Loading Click Handlers in Qwik with the onClick$ Suffix
Learn how Qwik’s onClick$ suffix splits click handlers into lazy‑loaded chunks, delivering zero‑hydration interactions with a simple build‑time signal.
19 Feb 2026, 15:07 UTC

Problem: Unnecessary JavaScript hurts interaction latency
When a page loads, many frameworks execute the entire component tree to attach event listeners. Even if a button is never clicked, its handler code is downloaded, parsed, and run during hydration, adding to Time‑to‑Interactive (TTI). In Qwik, the goal is to avoid this work entirely by loading only the code that is actually needed.
Thesis: The onClick$ suffix tells Qwik’s optimizer to split a click handler into its own lazy‑loaded chunk, serializing only a reference (QRL) into the HTML so the handler runs on first click without any hydration.
How the $ suffix works
During the Vite build, Qwik’s optimizer treats any function ending with $ as a boundary:
- The function (or component) is extracted into a separate JavaScript chunk.
- A QRL (Qwik Resource Locator) string is generated that points to that chunk.
- During SSR/SSG, only the QRL is inserted into the HTML as an attribute like
q:on:click. - The tiny
qwikloader(~1 KB) runs on the client, reads the attribute, and fetches the chunk only when the event occurs.
Because no component tree is re‑executed, there is zero hydration cost for that interaction.
Worked example: Adding a lazy‑loaded click handler
Assume you have a Qwik project created with npm create qwik@latest. You want a button that increments a counter stored in a signal.
import { component$, useSignal, onClick$ } from '@builder.io/qwik';
export const CounterButton = component$(() => {
const count = useSignal(0);
return (
{
// This handler will be lazy‑loaded
count.value++;
alert(`Count: ${count.value}`);
}}
>
Click me ({count})
);
});
To verify the split:
- Run the build:
npm run build(needs read/write access to the project directory). - Inspect the output:
ls dist/client/*.js. You should see a file similar toCounterButton.-<hash>.jsbesides the main entry. - Preview the production build:
npm run previewand view the page source. Search forq:on:click; its value will be a QRL like"#/chunk-<hash>.js". - Open Chrome DevTools → Network, enable "Disable cache", then click the button. The first click triggers a request for the lazy chunk; subsequent clicks reuse the cached chunk.
Checks: Confirm that the network request appears only after the first interaction and that the alert shows the updated count. Risks: If the handler captures non‑serializable values (e.g., a DOM node or a class instance from a third‑party library), the build will fail with a serialization error. Ensure all captured variables are plain objects, primitives, or signals.
Trade‑off: Developer ergonomics vs. granularity
The $ syntax eliminates manual import() calls, but it shifts the mental model: you must think about what belongs inside a $‑marked function. Over‑splitting can create many tiny chunks, increasing request overhead on low‑bandwidth connections. Conversely, under‑splitting (placing too much logic in a single $ function) defeats the purpose of lazy loading. A practical guideline is to keep each $ function focused on a single user interaction or a small piece of state‑dependent logic.
Actionable closing
Start by marking event handlers with onClick$ (or useTask$ for background work). After each build, inspect the generated chunks and the HTML attributes to confirm the QRL is present. Use the Network tab to verify lazy loading on first interaction. If you encounter a serialization error, refactor the captured values to be serializable or move the problematic code outside the $ boundary. This approach lets you reap Qwik’s zero‑hydration benefits while keeping the codebase approachable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.