DEV Community

loading...
Cover image for Qwik: the answer to optimal fine-grained lazy loading

Qwik: the answer to optimal fine-grained lazy loading

Miško Hevery
CTO at Builder.io, empower anyone to create blazing fast sites. Previously at Google, where he created Angular, AngularJS and was co-creator of Karma.
・7 min read

Qwik aims to delay the loading and executing of JavaScript for as long as possible and to only do so upon user action, to deliver the fastest loads. This is done during the initial load, as well as during the whole application lifetime. Put differently, Qwik wants to have fine-grained lazy-loading. By "fine-grained," we mean that only the code that is directly required to process user action will be downloaded. In this post, we’ll explore the technical challenges that need to be solved to achieve fine-grained lazy loading.

Serialize listeners

The most obvious challenge to solve is the initial page load. We’ve already covered how to do this in HTML first, JavaScript last. The trick is to serialize event name and event action as URL into the DOM attribute. A top-level global event handler can then listen to events and download the code associated with the event.

<button on:click="./MyComponent_onClick">click me</button>
Enter fullscreen mode Exit fullscreen mode

The above code achieves that with no JavaScript (outside of a 1kb loader) loaded on the initial page load. This solves the initial time-to-interactive goal, but creates a new issue. We don't want to cause full application download and bootstrap on the first user interaction. Doing so would only move the problem from initial load to initial interaction. (If anything, this would worsen the situation, because it would introduce significant latency to first user interaction.)

The solution is to ensure that no single user interaction causes a full application download or bootstrap. Instead, we only want to download and bootstrap/rehydrate the code/component which is directly needed to process the interactions. We want fine-grained lazy loading.

Serializing events into HTML/DOM is what makes this all possible. Without it, it would be impossible to delay the template from loading, because the framework would need to download the template to identify where the events are.

Asynchronous, out-of-order component hydration

To ensure that the first interaction does not cause a full application download and bootstrap, it is necessary to rehydrate the components asynchronously and out of order.

Here asynchronously means that the rendering system can pause rendering to asynchronously download a template for a component, and then continue the rendering process. This is in stark contrast to all of the existing frameworks, which have fully synchronous rendering pipelines. And because the rendering is synchronous, there is no place to insert asynchronous lazy-loading. The consequence is that all of the templates need to be present ahead of call to render.

Another issue with existing rehydration strategies is that they start at the root component and synchronously rehydrate all of the components below. The consequence is that all the components must be rehydrated at the same time, forcing the download of every component. This would cause an extended processing time for the first interaction. Out-of-order hydration means that each component can be rehydrated independently from any other component and in any order. This allows Qwik to only rehydrate the minimum number of components that are needed to process the request.

<div decl:template="./path/MyComponent_template">
  ... some content ...
</div>
Enter fullscreen mode Exit fullscreen mode

In the above case, <div> represents a component associated with MyComponent_template.ts. Qwik will only download the template if it determines that the component needs to be rerendered, thus further delaying its download.

Without out-of-order rehydration, the framework is forced to download all the templates and rehydrate them all at once. This would create large download and execution pressure on the first interaction.

Separation of rendering from event handlers

An essential consideration for Qwik is that all of the existing rendering systems inline event listeners into the template. The consequence of the above is that when a component needs to be rerendered (or rehydrated) the browser must also download all of the listeners, regardless of if they are required. The listeners often close over complex code, which further increases the amount of code that is downloaded.

import {complexFunction} from './large-dependency';

export function MyComponent() {
  return (
    <button onclick={() => complexFunction()}>
      rarely clicked => click handler downloaded eagerly
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Qwik separates the event handles from the template code. This means that either the listeners or template can be downloaded independently, and on an as-needed basis.

MyComponent_template.ts

export MyComponent_template() {
  return (
    <button on:click="./MyComponent_onClick">
      rarely clicked => click handler downloaded lazily
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

MyComponent_onClick.ts

import {complexFunction} from './large-dependency';

export default function() {
  complexFunction();
}
Enter fullscreen mode Exit fullscreen mode

Without the separation of event handlers from the templates, the framework would have to download a lot more code than required to rerender the component. Plus event handlers are often complex and have other dependencies, which adds to the amount of code that needs to be downloaded.

Serialization of component state

When a component gets rehydrated, an essential part of the process is to restore the component's state. Existing frameworks don't have a way to serialize the state, as there is no standard way to demarcate where the component's state is.

Qwik breaks components up into several pieces.

  • props: These are just properties of the components that are reflected in the DOM. For example: <counter min="0" max="100"/> the props are {min: 0, max: 100}.
  • state: Internal state of the component, which can be serialized into the DOM.
  • transient state: Any additional state that component may cache, but which cannot be serialized. This information needs to be recomputed (e.g. temporary promises while the component is talking to the server).
<div decl:template="./Conter_template"
     :.='{count: 42}'
     min="0" max="100">
  <button on:click="./Counter_incremente">+</button>
  42
  <button on:click="./Counter_decrement">+</button>
</div>
Enter fullscreen mode Exit fullscreen mode

If the component can’t serialize its state, it won’t be possible to rehydrate the specific component in isolation. (Where would the component get its state?) As a result, the framework would have to download extra code to compute or download the state from the server. Qwik avoids all of this by serializing state in the DOM.

Serialization of app/shared state

In addition to the component state, which is private to the component, the application state is also shared between components. It, too, needs to be serialized into the DOM. The shared state is broken down to:

  • key: An ID that uniquely identifies a piece of state. The ID is then used as a reference it in the components.
  • state: Shared state between the components which can be serialized into the DOM.
  • transient state: Any additional state that applications may cache, but can't be serialized. This information needs to be able to be recomputed.
<div :cart:="./Cart"
     cart:432="{items: ['item:789', 'item:987']}"
     :item:="./Item"
     item:789="{name: 'Shoe' price: '43.21'}"
     item:987="{name: 'Sock' price: '12.34'}">
  ...
</div>
Enter fullscreen mode Exit fullscreen mode

Serializing the application’s state allows the components to render the same information in multiple locations, and communicate with other components. Without the framework understanding and managing the shared state, it would not be possible to hydrate components independently because the framework would not know when the state changes. (For example, Angular and React don't have explicit state management tied to the render function. As a result, the only sensible thing to do when the application state changes is to rerender the entire application, which prevents fine-grained lazy-loading.)

Reactive connections between app state and components

The real benefit of having a framework that understands the state is that the framework knows the relationship between state and components. This is important because it tells the framework which component needs to be rehydrated when a given state changes. Or more importantly, it tells the framework which components don't need to be rehydrated when a state changes. For example, adding an item to a shopping cart should only rerender the component that displays the shopping cart count, which is only a tiny portion of the overall page.

<div :cart:="./Cart"
     cart:432="{items: ['item:789', 'item:987']}">
  <div decl:template="./Unrelated">...</div>
  <div decl:template="./ShoppingCart"
       bind:cart:432="$cart">
   2 items
  </div>
  <button on:click="./AddItem">buy</button>
</div>
Enter fullscreen mode Exit fullscreen mode

The goal of Qwik is to rehyrate the minimum number of components. When the user clicks on the <button> Qwik will download ./AddItem, updating the cart:432 application state. Qwik will then determine that a component with bind:cart:432 is the only component that uses the state, and therefore the only component which needs to be rehydrated and rerendered. Qwik can prune most of the components on the page, allowing it to keep the lazy-loading fine-grained. Knowing which components are connected to which state is a critical property that is not present in other frameworks. It is the property that allows for fine-grained lazy loading during application startup, as well as throughout its lifetime.

Component isolation

So far, we have discussed how Qwik supports fine-grained lazy loading of code. All of the above works because Qwik understands the data flow in the application. Qwik uses this information to prune the components that do not need to be rehydrated and only rehydrate the necessary components. The implication is that the components must not talk to other components without Qwik's knowledge. Components can't have secret conversations with other components.

If the components get a hold of state without Qwik's knowledge, Qwik would not know that the component needs to be rehydrated/rerendered when the state changes. This is why components need to list their dependencies in the component's properties explicitly.

Without explicit listing, the framework would have no choice but to rerender everything once the state changes. This would cause the whole application to be downloaded and boottraped.

Conclusion

There are many ways in which the approach to building web applications needs to change to structure it for lazy loading. The issue is that the current frameworks do not help with this problem, and sometimes they even make it worse (for example, forcing full-page rehydration, synchronous rendering, etc.). Qwik makes fine-grained lazy loading a reality, so that developers can build sites/apps that load in sub-seconds, no matter how large and complex they become.

Discussion (1)

Collapse
oz profile image
Evgenii OZ • Edited

Just found this part. And it explains everything what I didn't quite understand in the previous parts - thank you!

Interesting and brave idea, I especially like the part about the reactivity, controlled by the framework. Would be interesting to read about the communication between components. Also, curious to know if it's possible to create new components/templates dynamically (from the meta info, received from the server).