Optimizer
Qwik's philosophy is to delay loading code for as long as possible. To do that, Qwik relies on Optimizer to re-arrange the code for lazy loading. The Optimizer is code level transformation that runs as part of the rollup. (Optimizer is written in Rust (and available as WASM) for instant performance)
The Optimizer looks for $
and applies a transformation that extracts the expression following the $
and turns it into a lazy-loadable and importable symbol.
Let's start by looking at a simple Counter
example:
export const Counter = component$(() => {
const store = useStore({ count: 0 });
return <button onClick$={() => store.count++}>{store.count}</button>;
});
The above code represents what a developer would write to describe the component. Below are the transformations that the Optimizer applies to the code to make the code lazy-loadable.
const Counter = component(qrl('./chunk-a.js', 'Counter_onMount'));
chunk-a.js
:
export const Counter_onMount = () => {
const store = useStore({ count: 0 });
return <button onClick$={qrl('./chunk-b.js', 'Counter_onClick', [store])}>{store.count}</button>;
};
chunk-b.js
:
const Counter_onClick = () => {
const [store] = useLexicalScope();
return store.count++;
};
Notice that every occurrence of $
results in a new lazy loadable symbol.
$
and Optimizer Rules
Optimizer runs as part of the bundling step of building the application. 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.
$
The meaning of The Optimizer needs to know which expression should be extracted into a new file. Extracting a symbol is complicated because the reference to the symbol changes from direct to asynchronous loading. This means that Optimizer needs to cooperate with the runtime to know which symbols can be extracted and how the runtime can then load them.
Let's look at the hypothetical problem of acting on scroll. 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:
- 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. - The
onScroll
had to be implemented slightly differently as it needs to take into account the fact that theQRL
of the function needs to be loaded before it can be used. In practice usingqImport
is rare in Qwik application as the Qwik framework provides higher-level APIs that rarely expect the developer to work withqImport
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 a very 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 inMyComp
implementation details. - The application now has more entry points, giving the bundler more ways to chunk up the codebase.
See also: Capturing Lexical Scope.
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:
- The
QRL
inGreeter_onMount
now stores thesalutation
andprops
. This performs the role of capturing the constants inside closures. - The generated closure
Greeter_onRender
now has a preamble which restores thesalutation
andprops
(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.
Optimizer Rules
The Optimizer's job is to break up large applications into many small lazy-loadable chunks.
The Optimizer can lazy-load a function closure, which lexically captures variables. However, there are limits to what can be achieved, and therefore the Optimizer comes with a set of rules.
Since not all valid JavaScript is valid Optimizer code, keep in mind the following rules:
- All captured variables must be declared as a
const
. - All captured variables must be either:
- serializable
- importable (either
import
orexport
in this file)
The $
is not only a marker for the Optimizer but also a marker for the developer to follow these rules.
NOTE: There are plans for a linter that will be able to enforce these rules eagerly.
Imports
RULE: If a function that is being extracted by Optimizer refers to a top-level symbol, that symbol must either be imported or exported.
import { importedFn } from '...';
export exportedFn = () => {...};
const salutation = "Hello";
someApi$(() => {
importedFn(); // OK
exportedFn(); // OK
salutation; // Error: salutation not imported/exported
})
The reason for the above rule becomes obvious when the output is examined.
import { importedFn } from '...';
export exportedFn = () => { ... };
const salutation = "Hello";
someApi(qrl('./chunk-a.js', 'someApi_1'));
chunk-a.js
:
import { importedFn } from '...';
import { exportedFn } from './originalFile';
export const someApi_1 = () => {
importedFn(); // OK
exportedFn(); // OK
salutation; // Error: no way to get reference to this.
};
Closures
RULE: If a function lexically captures a variable (or parameter), that variable must be (1) a const
and (2) the value must be serializable.
function somefn() {
let count = 0;
list.foreach((item) => {
count++;
const currentCount = count;
someApi$(() => {
item; // OK (assuming serializable)
count; // ERROR: count not const
currentCount; // OK (assuming serializable)
});
});
}
Again looking at the generated code reveals why these rules must be so:
function somefn() {
let count = 0;
list.foreach((item) => {
count++;
const currentCount = count;
someApi$(qrl('./chunk-a.js', '_1', [item, count, currentCount]));
});
}
chunk-a.js
:
export _1 = () => {
const [item, count, currentCount] = useLexicalScope();
item; // OK (assuming serializable)
count; // ERROR: count not const
currentCount; // OK (assuming serializable)
};
See serialization for discussion of what is serializable.