Lazy Loading & Code Splitting

Qwik's core tenant is to delay the loading and execution of JavaScript for as long as possible. Qwik needs to break up the application into many lazy loadable chunks to achieve that.

Basic problem

Let's start by looking at a simple Counter example.

const Counter = component$(() => {
  const store = useStore({ count: 0 });

  return (
    <button onClick$={() => store.count++}>
      {store.count}
    </button>
  );
});

As a developer, it is desirable to put all parts of the component together into an easily readable format, as shown above. However, while the component is concise, it is not lazy-loadable.

Let's rewrite the component and give each function a name for easier discussion.

const Counter = component$(function onMount() {
  const store = useStore({count: 0});
  return $(function onRender() {
    return (
      <button onClick$={function onClick() {
        return store.count++;
      }>
        {store.count}
      </button>
    );
  });
});

NOTE: Code written verbosely for easier discussion.

Let's look at use cases:

  • initial render: will require onMount to create component state and onRender to render the component. onClick is not needed as no user interaction has occurred.
  • re-rendering: (assume store.count changes by another component, not shown.) Only onRender is needed. onMount is not required as the component already exists (no new state needs to be created.) onClick is not necessary because the user did not interact with the component.
  • interaction: Clicking on the button needs onClick, which will mutate the state.count, which will require onRender to update the component. onMount is only needed on component creation, and in this case, the component already exists.

As you can see, different use cases need different parts of the code. In the above trivial code example, downloading all of the code together is inconsequential. However, In large applications, it is easy to create large islands that are coupled and hence can't be lazy-loaded independently. The result is that bandwidth is wasted downloading code that is not immediately needed.

Optimizer

Optimizer (described in-depth here) is a code transformation that extracts functions into top-level importable symbols, which allows the Qwik runtime to lazy-load the JavaScript on a needed basis.

The Optimizer and Qwik runtime work together to achieve the desired result of fine-grained lazy loading.

Without Optimizer either:

  • The code would have to be broken up by the developer into importable parts. This would be unnatural to write an application, making for a bad DX.
  • The application would have to load a lot of unnecessary code as there would be no lazy-loaded boundaries.

Qwik runtime must understand the Optimizer output. The biggest difference to comprehend is that by breaking up the component into lazy-loadable chunks, the lazy-loading requirement introduces asynchronous code into the framework. The framework has to be written differently to take asynchronicity into account. Existing frameworks assume that all code is available synchronously. This assumption prevents an easy insertion of lazy-loading into existing frameworks. (For example, when a new component is created, the framework assumes that its initialization code can be invoked in a synchronous fashion. If this is the first time component is referenced, then its code needs to be lazy-loaded, and therefore the framework must take that into account.)

Lazy-loading

Lazy-loading is asynchronous. Qwik is an asynchronous framework. Qwik understands that at any time, it may not have a reference to a callback, and therefore, it may need to lazy-load it. (In contrast, most existing frameworks assume that all of the code is available synchronously, making lazy-loading non-trivial.)

In Qwik everything is lazy-loadable:

  • Component on-mount (initialization block)
  • Component on-watch (side-effects, only downloaded if inputs change)
  • Component on-render (only downloaded when a component needs to be re-rendered)
  • Listeners (only downloaded on interaction)
  • Styles (Only downloaded if the server did not already provide them)

Lazy-loading is a core property of the framework and not an afterthought.

Optimizer and $

Let's look at our example again:

const Counter = component$(() => {
  const store = useStore({ count: 0 });

  return (
    <button onClick$={() => store.count++}>
      {store.count}
    </button>
  );
});

Notice the presence of $ in the code. $ is a marker that tells the Optimizer that the function following it should be lazy-loaded. (For a detailed discussion see $ and Optimizer Rules.) The $ is a single character that hints to the Optimizer and the developer to let them know that asynchronous lazy-loading occurs here.