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,2]

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 state = useStore({ count: 0 });

  return <button onClick$={() => (state.count += props.step || 1)}>{state.count}</button>;
});

The optimizer breaks above code into pieces like so:

const Counter = component(qrl('./chunk-a.js', 'Counter_onMount'));

chunk-a.js:

export const Counter_onMount = (props) => {
  const store = useStore({ count: 0 });
  return qrl('./chunk-b.js', 'Counter_onRender', [store, props]);
};

chunk-b.js:

const Counter_onRender = () => {
  const [store, props] = useLexicalScope();
  return (
    <button onClick$={qrl('./chunk-c.js', 'Counter_onClick', [store, props])}>{store.count}</button>
  );
};

chunk-c.js:

const Counter_onClick = () => {
  const [store, props] = useLexicalScope();
  return (store.count += 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.

  1. HTML is loaded in the browser, and Qwikloader registers a global click listener. No other JavaScript is loaded/executed at this point.
  2. User clicks on the <button>. This fires a click event which bubbles up and is handled by the Qwikloader.
  3. Qwikloader retraces the event bubble path and looks for on:click attribute, which it finds on <button>.
  4. 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,2]"
    • <body q:base="/build/">
    • document.baseURI = "http://localhost/index.html"
    • The resulting absolute URL is http://localhost/build/chunk-c.js which Qwikloader fetches.
  5. Qwikloader now retrieves the Counter_onClick reference from http://localhost/build/chunk-c.js and invokes it.
    const Counter_onClick = () => {
      const [store, props] = useLexicalScope();
      return (store.count += props.step || 1);
    };
    
  6. 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.
  7. useLexicalScope is imported from @builder.io/qwik and is responsible for retrieving the store and props. const [store, props] = useLexicalScope();
  8. Parse the <script type="qwik/json">{...json...}</script> JSON and distribute the deserialized objects per q:obj attribute. In our case
    • <div q:id="123" q:obj="456" q:host> gets object with id 123. This will be the store created in the Counter_onMount function.
    • <button q:obj="456, 123" gets store as well as a reference to the <div q:id="457">
  9. Once the qwik/json is deserialized, useLexicalScope can use the QRL's [0,1] array to look into q:obj="456, 123" to retrieve object with id 456 and 123, which are props of <div q:id="123" q:obj="456" q:host> as well as store from Counter_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.

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:

  1. 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 the import('./some-path.js') so that it can be placed in the HTML.
  2. Dynamic imports deal with chunks, but they have no mechanism to refer to a specific symbol in the chunk.
  3. 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.
  4. QRLs encode information about the lexically scoped variables that were captured in the closure and need to be restored.
  5. 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.

See Also

  • qrl: API used to create QRLs.
Made with โค๏ธ by