DEV Community

loading...
Cover image for HTML-first, JavaScript last: the secret to web speed!

HTML-first, JavaScript last: the secret to web speed!

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.
・6 min read

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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>
Enter fullscreen mode Exit fullscreen mode
  • ::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 the q-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);
Enter fullscreen mode Exit fullscreen mode

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" ... >
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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.

Qwik Performance

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.

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!

Discussion (12)

Collapse
oz profile image
Evgenii OZ

Every state variable is inside the html? What about circular dependencies? What about functions, observables? They can’t be serialized.

Maybe I don't understand it clearly, but if I have some meta info about the items/devices/users, and this meta info size is 5 Mb - will it be pushed to the HTML just to save the state? Are there limits for attributes content length?

Collapse
mhevery profile image
Miško Hevery Author
  • Circular Dependencies: Can be serialized after the data is normalized. Think normalizing data to be stored in the database.
  • Observables: can be serialized as well. See description above how we have observables for data changes on the components and then use querySelectorAll to find the components/listeners.

Yes it involves a different way of thinking about the problem. I think the big point of these articles is that the industry is doing what is easy and as a result it ends up in hot water on TTI. We need to think about what is needed and adjust our development processes accordingly. Yes different is not trivial at first, but it does not have to be hard if you have the right guidance from the framework.

Collapse
oz profile image
Evgenii OZ

By observables I mean functions. I’ve re-read the post but still can’t find how they can be serialized. Please don't take it as criticism, I’m just trying to get the idea.

Right now I prefer to keep the state inside the rxjs observables, and if Qwik wants to “imprint” every state change into HTML, then all of these functions should be somehow serialized - I have no idea how to do this, but I admit that I don't know every trick existing.

Maybe Qwik is not going to save absolutely everything, maybe it should be the only amount of data, needed to “resurrect” the app from HTML attributes. Or maybe Qwik is not going to save the state on every change.

In the first case, it raises the question, how it will affect the performance if we are going to save-restore the state on every user action (or app internal event).

In the second case, the framework should be extremely smart to determine what state is important and what can be ignored. Or it should be controlled by the programmer.

Thread Thread
mhevery profile image
Miško Hevery Author

Those are all excellent questions but explanation would require more text than a response here. Will follow up with more blog posts where we will explore this. The short answer is that the framework keeps caches to make this fast, but caches are done in a way so that loosing them is not an issue for functionality, just performance.

Collapse
jakelauer profile image
Jake Lauer

This may be fast, but the developer experience would be hell. I can't imagine how frustrating this would be to scale.

Collapse
mhevery profile image
Miško Hevery Author

I don't think dev-exp NEEDS to be hell. Here is where the fromework comes in and helps you do the right thing and makes your dev-exp easy. Sounds like a perfect article to write next.

Collapse
jakelauer profile image
Jake Lauer

I would love to see it! I have found that the benefits of improving the developer experience often outweigh the performance reductions they portend. A world with both would be nice!

Collapse
scips profile image
Sébastien Barbieri

It's clearly nice to see it from this perspective. Most devs only think about their confort in programming. Here it is still easy to understand and use really simple principles.
But what about aspx. it looks like the aspx way of handling fullstate in the dom. The difference with aspx is that everything was sent back to the server, but the principles seems similar. While this one seems more human readable.

Collapse
ynk profile image
Julien Barbay

this page should not be a framework explanation but a 101 on how the web works.

Collapse
132 profile image
Yisar

It looks very weird, the serialization is through HTML, otherwise the HTML will be messy.

Collapse
zakiazfar profile image
Mohd Ahmad

What about astro, it is bring your own framework, or amp

Collapse
mhevery profile image
Miško Hevery Author

Yes astro is an interesting approach and can get you part of the way there. The issue is that as long as you retain monolithic way of thinking about the problem your options will be limited. If you are willing to re-think everything you can go a lot further. So think of astro as an improvment on the way to the ultimate speed.