Resumable vs. Hydration
A key concept of Qwik applications is that they are resumable from a server-side-rendered state. The best way to explain resumability is to understand how the current generation of frameworks are replayable (hydration).
When an SSR/SSG application boots up on a client, it requires that the framework on the client restores three pieces of information:
- Listeners - locate event listeners and install them on the DOM nodes to make the application interactive.
- Component tree - build up an internal data structure representing the application component tree.
- Application state - restore the application state.
Collectively, this is known as hydration. All current generations of frameworks require this step to make the application interactive.
Hydration is expensive for two reasons:
- The frameworks have to download all of the component code associated with the current page.
- The frameworks have to execute the templates associated with the components on the page to rebuild the listener location and the internal component tree.
Qwik is different because it does not require hydration to resume an application on the client. Not requiring hydration is what makes the Qwik application startup instantaneous.
All other frameworks' hydration replays all the application logic in the client. Qwik instead pauses execution in the server, and resumes execution in the client.
Resumability is about pausing execution in the server and resuming execution in the client without having to replay and download all of the application logic.
A good mental model is that Qwik applications at any point in their lifecycle can be serialized and moved to a different VM instance (server to browser). There, the application simply resumes where the serialization stopped. No hydration is required. This is why we say that Qwik applications don't hydrate; they resume.
In order to achieve this, Qwik needs to solve the 3 problems (listeners, component tree, application state) in a way that is compatible with a no-code startup.
DOM without event listeners is just a static page; it is not an application. Today's standard for all sites is quite a high level of interactivity, so even the most static-looking sites are full of event listeners. These include menus, hovers, expanding details, or even full-on interactive apps.
Existing frameworks solve the event listener by downloading the components and executing their templates to collect event listeners that are then attached to the DOM. The current approach has these issues:
- Requires the template code to be eagerly downloaded.
- Requires template code to be eagerly executed.
- Requires the event handler code to be downloaded eagerly (to be attached).
The above approach does not scale. As the application becomes more complicated, the amount of code needed to download eagerly and execute grows proportionally with the size of the application. This negatively impacts the application startup performance and hence the user experience.
Qwik solves the above issue by serializing the event listeners into DOM like so:
<button on:click="./chunk.js#handler_symbol">click me</button>
Qwik still needs to collect the listener information, but this step is done as part of the SSR/SSG. The results of SSR/SSG are then serialized into HTML so that the browser does not need to do anything to resume the execution. Notice that the
on:click attribute contains all of the information to resume the application without doing anything eagerly.
- Qwikloader sets up a single global listener instead of many individual listeners per DOM element. This step can be done with no application code present.
- The HTML contains a URL to the chunk and symbol name. The attribute tells Qwikloader which code chunk to download and which symbol to retrieve from the chunk.
- Finally, to make all of the above possible, Qwik's event processing implementation understands asynchronicity which allows insertion of asynchronous lazy loading.
Frameworks work with component trees. To that end, frameworks need a complete understanding of the component trees to know which components need to be rerendered and when. If you look into the existing framework SSR/SSG output, the component boundary information has been destroyed. There is no way of knowing where component boundaries are by looking at the resulting HTML. To recreate this information, frameworks re-execute the component templates and memoize the component boundary location. Re-execution is what hydration is. Hydration is expensive as it requires both the download of component templates and their execution.
Qwik collects component boundary information as part of the SSR/SSG and then serializes that information into HTML. The result is that Qwik can:
- Rebuild the component hierarchy information without the component code actually being present. The component code can remain lazy.
- Qwik can do this lazily only for the components which need to be re-rendered rather than all upfront.
- Qwik collects relationship information between stores and components. This creates a subscription model that informs Qwik which components need to be re-rendered as a result of state change. The subscription information also gets serialized into HTML.
Existing frameworks usually have a way of serializing the application state into HTML so that the state can be restored as part of hydration. In this way, they are very similar to Qwik. However, Qwik has state management more tightly integrated into the lifecycle of the components. In practice, this means that component can be delay-loaded independently from the state of the component. This is not easily achievable in existing frameworks because component props are usually created by the parent component. This creates a chain reaction. In order to restore component X, its parents need to be restored as well. Qwik allows any component to be resumed without the parent component code being present.
The simplest way to think about serialization is through
JSON.stringify. However, JSON has several limitations. Qwik can overcome some limitations, but some can't be overcome, and they place limitations on what the developer can do. Understanding these limitations is important when building Qwik applications.
Limitations of JSON which Qwik solves:
- JSON produces a DAG. DAG stands for Directed Acyclic Graph, which means that the object which is being serialized can't have circular references. This is a big limitation because the application state is often circular. Qwik ensures that when the graph of objects gets serialized, the circular references get properly saved and then restored.
- JSON can't serialize some object types. For example, DOM references, or Dates. Qwik's serialization format ensures that such objects can correctly be serialized and restored. Here is a list of types that can be serialized with Qwik:
- DOM references
- Promises (See resources)
- Function closures (if wrapped in QRL)
Limitations of JSON that Qwik does not solve:
- Serialization of classes (
- Serialization of Streams.
Writing applications with serializability in mind
The resumability capability of the framework must extend to resumability of the application as well. This means that the framework must provide mechanisms for the developer to express components and entities of the application in a way which can be serialized and then resumed (without re-bootstrapping). This necessitates that the application is written with resumability constraints in mind. It is simply not possible for developers to continue to write applications in a heap-centric way and expect that a better framework can somehow make up for this sub-optimal approach.
Developers must write their applications in a DOM-centric way. This will require a change of behavior and retooling of web developers' skills. Frameworks need to provide guidance and APIs to make it easy for developers to write the applications in this way.
Other benefits of resumability
The most obvious benefit of using resumability is for server-side-rendering. However, there are secondary benefits:
- Serializing existing PWA apps so that users don't lose context when they return to the application
- Improved rendering performance because only changed components need to be re-rendered
- Fine-grained lazy-loading
- Decreased memory pressure, especially on mobile devices
- Progressive interactivity of existing static websites