iJS CONFERENCE Blog

Resumability in JavaScript

An alternative to hydration

Jan 17, 2023

Fast could mean a lot of things, but in this article, we are concerned with startup performance. From the moment the user clicks on a link to navigate to a page until the page is ready for interaction. This contrasts with the update performance of how long it takes to update the UI when the user interacts with the page. The startup performance is where we could see the most benefit to user experience.

Defining fast!

Google cares a lot about startup performance because startup performance influences the user experience. For this reason, Google has built Core-Web-Vital metrics (CWV) that objectively measure the site’s user experience. Google said it would use CWV metrics in its SEO ranking algorithm to encourage the industry to move in the right direction.

Finally, a lot of data shows that startup performance matters for conversions, and Google has compiled a list of studies that show exactly that.

We used to be fast

Believe it or not, the web used to be fast! Well, fast in the startup performance sense. It used to be that every interaction required a full round trip from the client to the server, and the client had no JavaScript. It is widely accepted that the fastest way to get pixels in front of the user on initial navigation is through static HTML. So, what happened?

Sending HTML renders the page fast, but without JavaScript, we have no interactivity. So, from the early days, we have used JavaScript to improve the user interactivity story. This culminated with jQuery, which to this day is one of the most used packages on the web. By adding JavaScript, we could have pages that load fast and then are interactive.

The problem with that approach is that there is a lot of code duplication. The backend is written in one language (PHP, Ruby, Java, etc.), and the interaction in another (JavaScript). The backend needs to render a user contact form, and a client needs to add an additional address to the form. This means that both the client and server need the ability to render the same thing. You can see how quickly things go out of sync, not to mention that the developer has to constantly switch back and forth and keep related code in a different language and probably different locations.

While the DX of the above is not the best, surprisingly, it produces excellent performance characteristics. To this day, it is how Amazon serves its site.

What the above tweet shows is that serving pages that way, while not the best DX, produces the best UX.

iJS Newsletter

Keep up with JavaScript’s Vulnerabilities & Best Practices!

How we got slow

As described above, building sites by having a server render language and client render language is not ideal. If doing hybrid languages is hard, maybe we should do everything in just one language. Let’s move everything to JavaScript! This is the birth of Single Page Applications (SPA). The idea is simple: Serve a blank HTML that contains just enough to load JavaScript, then have the JavaScript render everything.

The SPA approach solves a lot of things from the DX perspective. Single language, single mental model, and it is easy enough to teach many developers quickly. So, the SPA took off. Now to be fair, the SPA has one thing going for it: Client-side interactions are fast, but not the startup performance.

At first, SPAs were small, so the startup performance was not too bad. The user would navigate to a page, would see a white screen, and a second later, the application would render. But over time, we kept adding more and more functionality to our applications. Animations, personalization, analytics, CSS in JS, component libraries, etc. Every time we added one of those features, the startup performance would get a bit worse because SPAs require that all of that code be present. Death by a thousand cuts. So, what started as a reasonable compromise quickly turned into an actual problem. Users had to wait too long on the white screen before the site would render.

Prerendering Hack

To make the sites appear as if they had a better startup performance, people started prerendering the applications on the server. So instead of sending you a blank HTML that bootstrapped your application, we prerendered the application into HTML on the server (or build time) and sent the HTML snapshot of the application to the client. Then on the client, the JavaScript would load as before, and once the JavaScript finished executing, the DOM generated by the HTML would be thrown away and replaced by the JavaScript render DOM. If we did this right, the user would not notice anything.

But this is just an illusion! We did not make the site faster. We only gave the user something to look at while the application was booting up. If the user interacts with the page before JavaScript replaces the DOM, the interaction will be ignored. Not the best user experience.

GAIN INSIGHTS INTO THE DO'S & DON'TS

Performance, Testing, & Security Hacks

Hydration

Very quickly, people realized that we could improve on the above hack. Instead of throwing away the DOM created by HTML and replacing it with the one generated by JavaScript, we could make the framework a bit smarter, and we could attempt to “reuse” the DOM that the HTML produces. By “reusing” the DOM, we would improve the user experience. If the user highlighted content or if the user entered text into a form, the “reuse” strategy would keep the user content and further improve the illusion that the site was fast.

The above hacks got on the official name of hydration, and we rebranded the hack into a feature.

The thing to stress is that the DOM created by the original HTML is not needed. If you deleted the DOM, most frameworks would continue to work because, as part of their hydration, they rebuild the DOM anyways.

SSR slows things down

As we pointed out above, hydration does not care if the DOM is present or not. It recreates the DOM anyways, attempting to reuse DOM nodes, if possible, but it is not strictly required. This means that the HTML we are sending now contains duplicate information and is, therefore, bigger. The information is sent to the client twice, once in the HTML and then again in JavaScript.

Browsers are very good at rendering HTML, but bigger HTML takes longer to download and renders than just a blank page, so this is not free. Hydration and site prerendering make the time to interaction actually worse, not better! It is only an illusion that things are better because the user has something to look at before they interact with the page.

Current approaches of prerendering HTML with hydration create an uncanny valley. An illusion that the page has loaded and that you can interact with it but attempting to interact produces no result. The page is dead, and there is no clear indication of when the page will become ready. On mobile devices and slow networks, it is not uncommon to see sites take upwards of 30 seconds to be ready for interaction.

Why is hydration needed?

Hydration is a way for a framework to recover information about the application so that the application can become interactive. What information? The application state is the most obvious, but there is much more. Frameworks also need the location of components, component props, event listeners, and reactivity graphs. Without that information, the frameworks can’t function. But that is not something most developers think about as it is an implementation detail of the framework. The frameworks must get that information somehow. Executing the application from beginning to end is how most frameworks collect the information because that is how we initially get the information: We just run the app.

But there is another way. During prerendering of the application on the server, the framework already had full knowledge of the location of components, component props, event listeners, and the reactivity graph. It is just that it was not serialized into HTML. If the framework could serialize the information and send it to the client along with HTML, then the same framework can recover the information on the client without resorting to the re-execution of the application on the client again.

Enter Resumability

Imagine you have a complex page with lots of components, and one of the components is a counter. Clicking on the counter increments its count. Notice that once the page has been initialized, only the counter handler and the counter rerendering code execute on the client. The fact that there are lots of other components on the page does not matter. Their code is not executed because they are not being interacted with.

The problem is getting hold of the counter listener. The framework had to start at a root component and visit every single component along the way until it got to the counter. That is a lot of JavaScript which needs to be downloaded and executed.

If only we could serialize the handler in such a way so that we can recover it without walking the component tree, then we would become resumable. The handler for the counter would be registered as part of server prerendering and would be executed on the client as part of the user interaction. No other code would have to execute.

If we could do that, then the amount of code we execute would be proportional to the user interaction rather than all possible interactions on the page. No code needs to be executed to get the page ready, and when the user finally interacts, the event handler can be directly invoked without pre-processing. This will make the page resumable because the next code to execute is the event handler.

 

Serializing closures

Modern frameworks make heavy use of closures. Closures are everywhere, but especially for event handlers. Initial execution of the application is partly expensive because the framework collects closures and attaches them to the event listeners in the DOM. Getting closures is tricky because they are, by definition, inlined into components. One can’t just import a closure, and even if one could, such closure would not have closed over its contextual variables. Yet getting hold of the closures efficiently is exactly what we need to make resumability work. How exactly this is done is beyond the scope of this article, but it is a key piece of the puzzle.

Serializing Reactivity Graph

Great, by serializing the closure, you can interact with the counter. However, unless the framework can identify which component needs to be rerendered, it may end up rerendering the whole application, which will force the execution and download of all the JavaScript, which resumability worked so hard to avoid. Non-reactive frameworks such as Angular and React will often rerender starting at the root component on state change. For this reason, a reactive system is needed.

The framework needs to be surgical about which components need to be rendered and when to keep the framework from rereading too much. Reactivity is the key. Reactive systems build up a graph that allows the framework to be extremely selective on which components rerender. This keeps the application rendering lean.

Does it matter?

The short answer is: Yes! Google has spent a lot of time collecting data that shows that time to interactive matters! This makes intuitive sense. You are browsing along, and something catches your eye. You click on the link and want to buy the latest shoes! You click the buy button, but it does not do anything. How long do you want to wait, before you give up and tell yourself that you don’t really need those shoes?

But my app built with hydration is fast!

There are a lot of sites demonstrating that you can build a fast site with your favorite technology. And you can! The problem is gradual. You start up fast, and as you add more features, the site becomes a tiny bit slower with each new feature until it is not fast anymore. I am yet to see a production e-commerce site that handles real traffic, and that scores well on Google CVW on mobile.

The question you should be asking is not whether you can build a demonstration site that is fast but whether it will remain fast at scale. Hydration is a cost that is proportional to the size of the application. Will your site remain fast as the size of the application grows?

Resumability has the nice property that its startup cost is constant. No matter how complex your site gets, the startup cost is always zero because there is no startup cost. It is resumable.

Resumability is not new

For almost a decade, Google has had an internal framework called Wiz that powers Google Search and Google Photos, among many other front-facing sites. Wiz is resumable, and it shows. Google Search is fast! As a user, it is not possible for you to interact with Google Search or Photos site before it is ready: It is instant!

eBay uses its own framework, Marko. While not historically resumable, resumability is a new feature that is part of their next release.

And, of course, Builder.io (http://builder.io), a visual CMS platform, is working on Qwik (http://qwik.builder.io). Qwik is the first open-source framework that brings resumability while maintaining great DX for everyone. Builder.io’s goal is to give our customers a competitive advantage through resumability, and as such, Qwik is a core part of our business.

Where are we heading?

We can’t continue building applications the way we have been until now. Yes, we have great DX, and our engineers are productive, but our UX suffers.

The average amount of JavaScript per site is going up because our customers expect more interactive experiences every year. This trend is not going to stop. It is a safe prediction that the amount of JavaScript will keep increasing.

The solution in front of us is not to write less JavaScript but to figure out how to spread the execution over time so that the browser does not get overwhelmed. After all, it is not possible for the user to interact with all the features of the site at the same time. So why do we insist on downloading and executing the whole site on startup?

The future can’t be more of the same. Companies such as Google, eBay, and Amazon showed that fast sites matter and that it makes business sense! For this reason, I think resumability will become the de facto way to build sites in the future. I don’t know if Qwik will become the preferred way to achieve that, but we hope so.

Once resumability becomes available to everyone, it will start an arms race. Currently, sites are slow, but so is everyone else’s, and few know how to make them fast or have the resources to do so. As soon as building fast sites becomes possible, an arms race will occur as everyone will try to get their sites fast. After all, Google CWV will give a competitive advantage to faster sites, and since building faster sites is no longer prohibitively expensive, companies will adopt it, putting pressure on the competition.

Summary

The web started fast by being declarative. The need for interactivity has pushed us towards JavaScript. In an effort to unify our DX, we have moved all of our development to JavaScript and created SPAs. SPAs have great DX, but their startup performance is proportional to page complexity. As pages are getting more complex, they are becoming slower. Prerendering a page at the client is a workaround to give the user the illusion of a faster site, but it actually decreases the startup performance. By reusing the DOM nodes, we have turned the workaround into a feature and called it hydration.

Resumability is an alternative to hydration because it can use the information the server had when it prerendered the HTML to continue executing where the server left off. This almost eliminates the JavaScript that needs to be executed on page startup, resulting in instant apps.

Qwik is not the only framework that is resumable, but it is the first to make resumability its key selling point. The future belongs to resumability because it can significantly increase the conversion rates of the site, which leads to more profit for the business. We think resumability will start an arms race as a faster site will create a strong competitive advantage and force other businesses to implement the same technology to stay competitive.

STAY TUNED!

 

BEHIND THE TRACKS OF iJS

Angular

Best-Practises with Angular

Vue.js

One of the most famous frameworks of modern days

JavaScript Practices & Tools

DevOps, Testing, Performance, Toolchain & SEO

Node.js

All about Node.js

React

From Basic concepts to unidirectional data flows