The dollar $ sign

Qwik splits up your application into many small pieces we call symbols. A component can be broken up into many symbols, so a symbol is smaller than a component. The splitting up is performed by the Qwik Optimizer.

The $ prefix is used to signal both the optimizer and the developer when this transformation occurs. As a developer, you need to understand that special rules apply whenever you see $ (not all valid JavaScript is a valid Qwik Optimizer transform.)

Compiler-time implications

Optimizer runs as a Vite plugin during bundling. The purpose of the Optimizer is to break up the application into many small lazy-loadable chunks. The Optimizer moves expressions (usually functions) into new files and leaves behind a reference pointing to where the expression was moved from.

The $ tells the optimizer which functions to extract into a separate file up and which ones to leave untouched. The optimizer does not keep an internal list of magic functions, instead, it solely relies on the $ suffix to know which functions to transform. The system is extendable and developers can create their own $ functions, such as myCustomFunction$().

Example

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

export default component$(() => {
  console.log('render');
  return <p onClick$={() => console.log('hello')}>Hello Qwik</p>;
});

The component above is split thanks to the $ syntax into:

app.js

import { componentQrl, qrl } from '@builder.io/qwik';

const App = /*#__PURE__*/ componentQrl(
  qrl(() => import('./app_component_akbu84a8zes.js'), 'App_component_AkbU84a8zes')
);

export { App };

app_component_akbu84a8zes.js

import { jsx as _jsx } from '@builder.io/qwik/jsx-runtime';
import { qrl } from '@builder.io/qwik';
export const App_component_AkbU84a8zes = () => {
  console.log('render');
  return /*#__PURE__*/ _jsx('p', {
    onClick$: qrl(
      () => import('./app_component_p_onclick_01pegc10cpw'),
      'App_component_p_onClick_01pEgC10cpw'
    ),
    children: 'Hello Qwik',
  });
};

app_component_p_onclick_01pegc10cpw.js

export const App_component_p_onClick_01pEgC10cpw = () => console.log('hello');

Rules

The optimizer uses $ as a signal to extract the code. The developer needs to understand that the extraction comes with constraints and therefore special rules apply whenever the $ is present. (Not all valid JavaScript code is valid code for the Optimizer.)

The worst kind of code magic is the kind that's invisible to the developer.

Allowed expressions

The first argument of any function that ends with $ have certain restrictions:

1. Literals without local identifiers

Invalid

const foo = 'foo';
foo$({ value: foo }); // it contains a local identifier "foo"

Invalid

const bar = 'bar';
foo$(`Hello, ${bar}`); // it contains a local identifier "bar"

Invalid

const count = 0;
foo$(count + 1); // it contains a local identifier "count"

Valid

foo$(`Hello, bar`);
foo$({ value: 'stuff' });
foo$(1 + 3);

2. Importable identifiers

Invalid

const foo = 'foo';
foo$(foo); // foo is not exported, so it's not importable

Valid

export const bar = 'bar';
foo$(bar);

Valid

import { bar } from './bar';
foo$(bar);

3. Closures

For closures, the rules are a bit relaxed, and local identifiers can be referenced and captured.

RULE: If a function lexically captures a variable (or parameter), that variable must be:

  1. a const and
  2. the value must be serializable.
3.1. Captured variables must be declared as a const.

Invalid

component$(() => {
  let foo = 'value'; // variable is not a const
  return <div onClick$={() => console.log(foo)}>
});

Valid

component$(() => {
  const foo = 'value';
  return <div onClick$={() => console.log(foo)}>
});
3.2. Local captured variables must be serializable

Invalid

component$(() => {
  const foo = new MyCustomClass(12); // MyCustomClass is not serializable
  return <div onClick$={() => console.log(foo)}>
});

Valid

component$(() => {
  const foo = { data: 12 };
  return <div onClick$={() => console.log(foo)}>
});
3.3. Module-declared variables can be importable

RULE: If a function that is being extracted by Optimizer refers to a top-level symbol, that symbol must either be imported or exported.

Valid

import { foo } from './foo';
component$(() => {
  console.log(foo);
});

Valid

export const foo = new MyCustomClass(12);
component$(() => {
  console.log(foo);
});

Invalid

// Foo is declared at the module level, but it's not exported
const foo = new MyCustomClass(12);
component$(() => {
  console.log(foo);
});

Deep dive

Let's look at the hypothetical problem of acting on a scroll event. You may be tempted to write the code like so:

function onScroll(fn: () => void) {
  document.addEventListener('scroll', fn);
}

onScroll(() => alert('scroll'));

The problem with this approach is that the event handler is eagerly loaded, even if the scroll event never triggers. What is needed is a way to refer to code in a lazy loadable way.

The developer could write:

export scrollHandler = () => alert('scroll');

onScroll(() => (await import('./some-chunk')).scrollHandler());

This works but is a lot of work. The developer is responsible for putting the code in a different file and hard coding the chunk name. Instead, we use Optimizer to perform the work for us automatically. But we need a way to tell Optimizer that we want to perform such a refactoring. We use $() as a marker function for this purpose.

function onScroll(fnQrl: QRL<() => void>) {
  document.addEventListener('scroll', async () => {
    fn = await qImport(document, fnQrl);
    fn();
  });
}

onScroll($(() => alert('clicked')));

The Optimizer will generate:

onScroll(qrl('./chunk-a.js', 'onScroll_1'));

chunk-a.js:

export const onScroll_1 = () => alert('scroll');

Notice:

  1. All that the developer had to do was to wrap the function in the $() to signal to the Optimizer that the function should be moved to a new file and therefore lazy-loaded.
  2. The onScroll had to be implemented slightly differently as it needs to take into account the fact that the QRL of the function needs to be loaded before it can be used. In practice using qImport is rare in Qwik applications as the Qwik framework provides higher-level APIs that rarely expect the developer to work with qImport directly.

However, wrapping code in $() is a bit inconvenient. For this reason, Optimizer implicitly wraps the first argument of any function call, which ends with $. (Additionally, one can use implicit$FirstArg() to automatically perform the wrapping and type matching of the function taking the QRL.)

const onScroll$ = implicit$FirstArg(onScroll);

onScroll$(() => alert('scroll'));

Now the developer has an easy syntax for expressing that a particular function should be lazy-loaded.

Symbol extraction

Assume that you have this code:

export const MyComp = component$(() => {
  /* my component definition */
});

The Optimizer breaks the code up into two files:

The original file:

const MyComp = component(qrl('./chunk-a.js', 'MyComp_onMount'));

chunk-a.js:

export const MyComp_onMount = () => {
  /* my component definition */
};

The result of Optimizer is that the MyComp's onMount method was extracted into a new file. There are a few benefits to doing this:

  • A Parent component can refer to MyComp without pulling in MyComp implementation details.
  • The application now has more entry points, giving the bundler more ways to chunk up the codebase.

Capturing the lexical scope

The Optimizer extracts expressions (usually functions) into new files and leaves behind a QRL pointing to the lazy-loaded location.

Let's look at a simple case:

export const Greeter = component$(() => {
  return <span>Hello World!</span>;
});

this will result in:

const Greeter = component(qrl('./chunk-a.js', 'Greeter_onMount'));

chunk-a.js:

const Greeter_onMount = () => {
  return qrl('./chunk-b.js', 'Greeter_onRender');
};

chunk-b.js:

const Greeter_onRender = () => <span>Hello World!</span>;

The above is for simple cases where the extracted function closure does not capture any variables. Let's look at a more complicated case where the extracted function closure lexically captures variables.

export const Greeter = component$((props: { name: string }) => {
  const salutation = 'Hello';

  return (
    <span>
      {salutation} {props.name}!
    </span>
  );
});

The naive way to extract functions will not work.

const Greeter = component(qrl('./chunk-a.js', 'Greeter_onMount'));

chunk-a.js:

const Greeter_onMount = (props) => {
  const salutation = 'Hello';
  return qrl('./chunk-b.js', 'Greeter_onRender');
};

chunk-b.js:

const Greeter_onRender = () => (
  <span>
    {salutation} {props.name}!
  </span>
);

The issue can be seen in chunk-b.js. The extracted function refers to salutation and props, which are no longer in the lexical scope of the function. For this reason, the generated code must be slightly different.

chunk-a.js:

const Greeter_onMount = (props) => {
  const salutation = 'Hello';
  return qrl('./chunk-b.js', 'Greeter_onRender', [salutation, props]);
};

chunk-b.js:

const Greeter_onRender = () => {
  const [salutation, props] = useLexicalScope();

  return (
    <span>
      {salutation} {props.name}!
    </span>
  );
};

Notice two changes:

  1. The QRL in Greeter_onMount now stores the salutation and props. This performs the role of capturing the constants inside closures.
  2. The generated closure Greeter_onRender now has a preamble which restores the salutation and props (const [salutation, props] = useLexicalScope().)

The ability for the Optimizer (and Qwik runtime) to capture lexically scoped constants significantly improves which functions can be extracted into lazy-loaded resources. It is a powerful tool for breaking up complex applications into smaller lazy-loadable chunks.

Serialization

See serialization for discussion of what is serializable.

Made with ❤️ by