Qwik's $ Prefix: How One Character Decides When Your JavaScript Ships
Qwik's $ suffix is the entire lazy-loading mechanism in one character. Here's what the optimizer does with it, how to verify it works, and the footgun of forgetting it.
28 Feb 2026, 23:39 UTC

Most JavaScript frameworks make you pay for interactivity up front: the browser downloads the whole bundle, replays your component tree during hydration, and only then can a button respond. Qwik takes a different bet, and the entire mechanism is visible in your source code as a single character: $. Understanding what that character actually does — and what happens when you forget it — is the difference between a Qwik app that feels instant and one that quietly ships the same megabytes you were trying to avoid.
The short version: $ marks a function whose body should be extracted into a separate, lazily loaded chunk. Qwik serializes a reference to that function into the HTML, and the real code downloads only when the associated event fires. No hydration pass, no replaying the tree.
What the optimizer does with $
Qwik's build-time optimizer scans your code for $-suffixed functions and splits them out of the initial bundle. When you write an event handler like onClick$, the function body becomes its own module. The server-rendered HTML contains a serialized pointer to it — essentially a placeholder that says "when a click happens on this element, fetch chunk X and run symbol Y."
Because the application's state and component structure are serialized into the HTML itself, Qwik can "resume" where the server left off instead of re-executing everything in the browser. That's what the framework means by resumability, and it's why Time to Interactive stays low even on slow devices: the browser does almost no JavaScript work on page load.
A concrete example
Here's a minimal counter. You can scaffold a project with npm create qwik@latest and drop this into a route component:
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const count = useSignal(0);
return (
<button onClick$={() => {
count.value++;
console.log('clicked', count.value);
}}>
Count: {count.value}
</button>
);
});Notice $ appears twice: component$ marks the component itself as a lazy-loadable boundary, and onClick$ marks the handler. Run npm run dev, open the browser's network tab filtered to JS, and click the button. You'll see a small chunk fetched at click time — that's the handler body arriving on demand. The initial page load's JavaScript is tiny by comparison.
This is the verification habit worth building: don't trust that the $ is working, watch the network tab. If the handler's code arrives with the initial page load, something broke the lazy boundary.
The footgun: forgetting the $
The most common mistake is also the quietest one. If you pass a regular function as a handler — or import a helper and reference it directly without wrapping it — that code is no longer lazily loaded. It gets pulled into the eager graph and downloaded up front, along with everything it imports. One unmarked handler that touches a heavy utility library can drag the whole library into the initial payload.
The same applies to the primitives around your handlers. useVisibleTask$, useTask$, event props ending in $ — the convention is consistent, and the optimizer only splits where you tell it to. When you need to wrap an existing function, Qwik provides $() explicitly:
import { $ } from '@builder.io/qwik';
import { trackEvent } from './analytics';
const handleClick = $(() => {
trackEvent('cta_click');
});A practical audit: build for production, compare the total initial JS with and without your $ markers intact, and use the Qwik DevTools extension to inspect which symbols are resumable. If the serialized HTML contains real function bodies instead of placeholder references, you've lost the boundary somewhere.
Trade-offs worth knowing
Lazy loading per interaction introduces a small latency on first trigger — the chunk has to arrive before the handler runs. Qwik mitigates this with prefetching strategies (speculatively fetching likely-needed chunks during idle time), but on a very slow connection, a user's first click can still wait on the network. For interactions where even that delay is unacceptable, you may deliberately keep critical handlers eager.
Debugging also changes shape. Breakpoints in a lazy handler won't hit until the chunk is fetched and executed, and stack traces cross chunk boundaries. Enable source maps in your dev build and expect the mental model to feel different from a monolithic bundle — the code you're stepping through genuinely wasn't in the browser a moment ago.
Finally, closures matter. A $ function can only capture serializable values, because Qwik has to serialize the captured context into the HTML. Capturing a DOM node, a class instance with methods, or a live socket will fail or behave unexpectedly. Keep captured state in signals and plain data.
Where to start
Pick one interactive element in an existing Qwik app — a button, a form submit — and confirm its handler carries the $. Then open the network tab, click it, and watch the chunk arrive. That single observation teaches the model better than any diagram: $ isn't decoration, it's the exact line between "ships now" and "ships when needed." Once you can see that line in your own bundle, the rest of Qwik's design starts to make sense.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.