QRL
QRL (Qwik URL) is a particular form of URL that Qwik uses to lazy load content.
QRLs:
- are specially formatted URLs that are left as attributes in the HTML to tell Qwik where the handlers for the code should be loaded from.
- point to a JavaScript chunk to be lazy-loaded.
- Contain a symbol name which needs to be retrieved from the chunk.
- May contain lexically scoped object references. (Captured variables from closures.)
- If relative, use
q:base
for resolution.
QRL Encoding
./path/to/chunk.js#SymbolName
In its simplest form, the QRL contains a URL (such as ./path/to/chunk.js
) that the browser can use to lazy-load a resource, and a SymbolName
to retrieve from the lazy-loaded chunk.
Qwik uses q:base
to resolve a QRL into an absolute URL if the URL is relative. (If no q:base
attribute is present, then document.baseURI
is used as a base.)
Encoding lexically scoped captured variables
QRLs can also restore lexically scoped variables. In that case, the variables are encoded in the QRL at the end in the form of an array of indexes into the q:obj
attribute.
./path/to/chunk.js#SymbolName[0,1]
The array is used by useLexicalScope()
to restore the variables.
Example
Let's look at an example of how all of the pieces of the QRL tie together.
The developer writes code for a simple component.
export const Counter = component$((props: { step: number }) => {
const count = useSignal(0);
return <button onClick$={() => (count.value += props.step || 1)}>{count.value}</button>;
});
The optimizer breaks above code into pieces like so:
const Counter = component(qrl('./chunk-a.js', 'Counter_onMount'));
export const Counter_onMount = (props) => {
const count = useSignal(0);
return qrl('./chunk-b.js', 'Counter_onRender', [count, props]);
};
const Counter_onRender = () => {
const [count, props] = useLexicalScope();
return (
<button onClick$={qrl('./chunk-c.js', 'Counter_onClick', [count, props])}>{count.value}</button>
);
};
const Counter_onClick = () => {
const [count, props] = useLexicalScope();
return (count.value += props.step || 1);
};
Rendered HTML
After the above code gets executed, it produces this HTML.
Assume: http://localhost/index.html
<html>
<body q:base="/build/">
<button q:obj="456, 123" on:click="./chunk-c.js#Counter_onClick[0,1]">0</button>
<script>
/*Qwikloader script*/
</script>
<script type="qwik/json">
{...json...}
</script>
</body>
</html>
The main thing to note is the on:click
attribute. This attribute gets read by the Qwikloader when the user clicks on the button.
- HTML is loaded in the browser, and Qwikloader registers a global
click
listener. No other JavaScript is loaded/executed at this point. - User clicks on the
<button>
. This fires aclick
event which bubbles up and is handled by the Qwikloader. - Qwikloader retraces the event bubble path and looks for
on:click
attribute, which it finds on<button>
. - Qwikloader now tries to load the corresponding chunk. To do that, the Qwikloader needs to resolve the relative path of
./chunk-c.js
. It uses these values to build an absolute path starting at<button>
and walking towards the document.on:click="./chunk-c.js#Counter_onClick[0,1]"
<body q:base="/build/">
document.baseURI = "http://localhost/index.html"
- The resulting absolute URL is
http://localhost/build/chunk-c.js
which Qwikloader fetches.
- Qwikloader now retrieves the
Counter_onClick
reference fromhttp://localhost/build/chunk-c.js
and invokes it.const Counter_onClick = () => { const [count, props] = useLexicalScope(); return (count.value += props.step || 1); };
- At this point, the execution is handed off from Qwikloader to the lazy-loaded chunk. This is done so that the Qwikloader can be as small as possible as it is inlined into the HTML.
useLexicalScope
is imported from@builder.io/qwik
and is responsible for retrieving thecount
andprops
.const [count, props] = useLexicalScope();
- Parse the
<script type="qwik/json">{...json...}</script>
JSON and distribute the deserialized objects perq:obj
attribute. In our case<div q:id="123" q:obj="456" q:host>
gets object with id123
. This will be thecount
created in theCounter_onMount
function.<button q:obj="456, 123"
getscount
as well as a reference to the<div q:id="457">
- Once the
qwik/json
is deserialized,useLexicalScope
can use the QRL's[0,1]
array to look intoq:obj="456, 123"
to retrieve object with id456
and123
, which are props of<div q:id="123" q:obj="456" q:host>
as well asstore
fromCounter_onMount
function.
NOTE: For performance reasons the
q:obj
and<script type="qwik/json">
are only updated when the application is deserialized into HTML. When the application is running, these attributes may have stale values.
import()
?
Why not just dynamic Browsers already have dynamic import mechanisms available from import()
. Why not use that instead of inventing a new QRL format?
There are several reasons:
- For Qwik to work, the QRLs need to be serialized into HTML. This is problematic for dynamic
import()
because there is no easy way to retrieve the relative URL from theimport('./some-path.js')
so that it can be placed in the HTML. - Dynamic imports deal with chunks, but they have no mechanism to refer to a specific symbol in the chunk.
- Dynamic imports have relative paths to the file which imported it. This is a problem because when the relative path is placed in HTML, it loses the context which defines relative to what. When the framework reads the path from HTML and tries to import it as
import(element.getAttribute('on:click'))
, the framework will become the context for relative path resolution. This is wrong as the original context before serialization to the HTML was different. - QRLs encode information about the lexically scoped variables that were captured in the closure and need to be restored.
- Dynamic import requires that the developer writes
import('./file-a.js')
, which means the developer is in charge of deciding where the lazy-loaded boundaries are. This limits the ability of the tooling to move code around in an automated way.
Because of the above differences, Qwik introduces QRLs as a mechanism for lazy loading closures into a Qwik application.