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:basefor resolution.
QRL Encoding
./path/to/chunk.js#SymbolNameIn 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
clicklistener. No other JavaScript is loaded/executed at this point. - User clicks on the
<button>. This fires aclickevent which bubbles up and is handled by the Qwikloader. - Qwikloader retraces the event bubble path and looks for
on:clickattribute, 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.jswhich Qwikloader fetches.
- Qwikloader now retrieves the
Counter_onClickreference fromhttp://localhost/build/chunk-c.jsand 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.
useLexicalScopeis imported from@builder.io/qwikand is responsible for retrieving thecountandprops.const [count, props] = useLexicalScope();- Parse the
<script type="qwik/json">{...json...}</script>JSON and distribute the deserialized objects perq:objattribute. In our case<div q:id="123" q:obj="456" q:host>gets object with id123. This will be thecountcreated in theCounter_onMountfunction.<button q:obj="456, 123"getscountas well as a reference to the<div q:id="457">
- Once the
qwik/jsonis deserialized,useLexicalScopecan use the QRL's[0,1]array to look intoq:obj="456, 123"to retrieve object with id456and123, which are props of<div q:id="123" q:obj="456" q:host>as well asstorefromCounter_onMountfunction.
NOTE: For performance reasons the
q:objand<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.
Why not just dynamic import()?
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.