HTML-first, JavaScript last: the secret to web speed!
Miško Hevery
Posted on July 14, 2021
All frameworks need to keep state. Frameworks build up the state by executing the templates. Most frameworks keep this state in the JavaScript heap in the form of references and closures. What is unique about Qwik is that the state is kept in the DOM in the form of attributes. (Note that neither references nor closures are wire serializable, but DOM attributes, which are strings, are. This is key for resumability!)
The consequences of keeping state in the DOM have many unique benefits, including:
- DOM has HTML as its serialization format. By keeping state in the DOM in the form of string attributes, the application can be serialized into HTML at any point. The HTML can be sent over the wire and deserialized to DOM on a different client. The deserialized DOM can then be resumed.
- Each component can be resumed independently from any other component. This out-of-order rehydration allows only a subset of the whole application to be rehydrated and limits the amount of code that needs to be downloaded as a response to user action. This is quite different from traditional frameworks.
- Qwik is a stateless framework (all application states are in DOM in the form of strings). Stateless code is easy to serialize, ship over the wire, and resume. It is also what allows components to be rehydrated independently from each other.
- The application can be serialized at any point in time (not just on initial render), and many times over.
Let's look at a simple Counter
component example, and how state serialization works. (Note that this is the output of the server-side rendered HTML, not necessarily specific code developers would be hand-coding.)
<div ::app-state="./AppState"
app-state:1234="{count: 321}">
<div decl:template="./Counter_template"
on:q-render="./Counter_template"
::.="{countStep: 5}"
bind:app-state="state:1234">
<button on:click="./MyComponent_increment">+5</button>
321.
<button on:click="./MyComponent_decrrement">-5</button>
</div>
</div>
-
::app-state
(application state code): Points to a URL where the application state mutation code can be downloaded. The state update code is only downloaded if a state needs to be mutated. -
app-state:1234
(application state instance): A pointer to a specific application instance. By serializing the state, the application can resume where it left off, rather than replaying the rebuilding of the state. -
decl:template
(declare template): Points to a URL where the component template can be downloaded. The component template is not downloaded until Qwik determines that the component's state has changed, and needs to be rerendered. -
on:q-render
(component is scheduled for rendering): Frameworks need to keep track of which components need to be rerendered. This is usually done by storing an internal list of invalidated components. With Qwik, the list of invalidated components is stored in the DOM in the form of attributes. The components are then waiting for theq-render
event to broadcast. -
::.="{countStep: 5}"
(Internal state of component instance): A component may need to keep its internal state after rehydration. It can keep the state in the DOM. When the component is rehydrated it has all of the state it needs to continue. It does not need to rebuild its state. -
bind:app-state="state:1234"
(a reference to shared application state): This allows multiple components to refer to the same shared application state.
querySelectorAll
is our friend
A common thing that a framework needs to do is to identify which components need to be rerendered when the state of the application changes. This can happen as a result of several reasons, such as a component has been invalidated explicitly (markDirty()
), or because a component is invalidated implicitly because the application shared state has changed.
In the example above, the count
is kept in the application state under the key app-state:1234
. If the state is updated it is necessary to invalidate (queue for rerender) the components that depend on that application state. How should the framework know which components to update?
In most frameworks the answer is to just rerender the whole application, starting from the root component. This strategy has the unfortunate consequence that all the component templates need to be downloaded, which negatively affects latency on user interaction.
Some frameworks are reactive and keep track of the component that should be rerendered when a given state changes. However, this book-keeping is in the form of closures (see Death By Closure) which close over the templates. The consequence is that all the templates need to be downloaded at the application bootstrap when the reactive connections are initialized.
Qwik is component-level reactive. Because it is reactive, it does not need to render starting at the root. However, instead of keeping the reactive listeners in the form of closures, it keeps them in the DOM in the form of attributes, which allows Qwik to be resumable.
If count
gets updated, Qwik can internally determine which components need to be invalidated by executing this querySelectorAll
.
querySelectorAll('bind\\:app-state\\:1234').forEach(markDirty);
The above query allows Qwik to determine which components depend on the state, and for each component it invokes markDirty()
on it. markDirty()
invalidates the component and adds it to a queue of components which need to be rerendered. This is done to coalesce multiple markDirity
invocations into a single rendering pass. The rendering pass is scheduled using requestAnimationFrame
. But, unlike most frameworks, Qwik keeps this queue in the DOM in the form of the attribute as well.
<div on:q-render="./Counter_template" ... >
requestAnimationFrame
is used to schedule rendering. Logically, this means that requestAnimationFrame
broadcasts the q-render
event which the component is waiting on. Again querySelectorAll
comes to the rescue.
querySelectorAll('on\\:q-render').forEach(jsxRender);
Browsers do not have broadcast events (reverse of event bubbling), but querySelectorAll
can be used to identify all the components which should receive the event broadcast. jsxRender
function is then used to rerender the UI.
Notice that at no point does Qwik need to keep state outside of what is in the DOM. Any state is stored in the DOM in the form of attributes, which are automatically serialized into HTML. In other words, at any time the application can be snapshot into HTML, sent over the wire, and deserialized. The application will automatically resume where it left off.
Qwik is stateless, and it is this that makes Qwik applications resumable.
Benefits
Resumability of applications is the obvious benefit of storing all framework state in DOM elements. However, there are other benefits which may not be obvious at first glance.
Skipping rendering for components which are outside of the visible viewport. When a q-render
event is broadcast to determine if the component needs to be rendered, it is easy to determine if the component is visible and simply skip the rendering for that component. Skipping the rendering also means that no template, or any other code, is required to be downloaded.
Another benefit of statelessness is that HTML can be lazy loaded as the application is already running. For example, the server can send the HTML for rendering the initial view, but skip the HTML for the view which is not visible. The user can start interacting with the initial view and use the application. As soon as the user starts scrolling the application can fetch more HTML and innerHTML
it at the end of the DOM. Because Qwik is stateless, the additional HTML can be just inserted without causing any issues to the already running application. Qwik does not know about the new HTML until someone interacts with it, and only then it gets lazy hydrated. The use case described above is very difficult to do with the current generation of frameworks.
We are very excited about the future of Qwik, and the kind of use cases that it opens up.
- Try it on StackBlitz
- Star us on github.com/builderio/qwik
- Follow us on @QwikDev and @builderio
- Chat us on Discord
- Join builder.io
That’s it for now, but stay tuned as we continue to write about Qwik and the future of frontend frameworks in the coming weeks!
Posted on July 14, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.