No one wants a slow, unresponsive website. Prerendering your site is one of your most powerful tools as a web developer when it comes to website performance optimization.
However, there’s a problem. HTML is static whereas websites are usually dynamic and interactive. How do frameworks make pre-rendered HTML interactive on the browser?
The current generation of frameworks solves this through a client-side process called hydration, a resource-intensive process that adds significant overhead to the page’s startup cost.
Here’s my controversial opinion: hydration is what happens when you add SSR/SSG as an afterthought to a front-end framework.
A framework designed from the ground up for prerendering can avoid hydration and its performance penalty by serializing state on the server and resuming it on the client.
Before diving into serialization and resumability, let’s talk about the problem that hydration solves.
Any front-end framework needs three things to be able to respond to interactivity:
Let's look deeper into how hydration handles these tasks and why it’s an expensive approach.
Frameworks associate event handlers with specific DOM elements by executing a component template. For example, in React, a button component written in JSX might have an onClick prop with an event handler. Hydration requires that the browser downloads and executes all components’ templates before associating event handlers.
Unfortunately, the JS bundle’s download size and code execution time is proportional to the complexity of the page. A small demo page will download a small amount of JS and execute quickly, but the bootstrap cost becomes prohibitively expensive when it comes to real-world pages, often leading to multi second times to interactive (TTI).
Some frameworks mitigate this performance penalty by attempting to delay when certain parts of a page are rendered. This strategy works reasonably well for content-centric pages such as marketing pages. For sites like web apps where components share state in complex ways, however, frameworks still need to download every component in the DOM tree and execute its template.
Event handlers need an application state to update, which is present on the server during prerendering. Frameworks must reconstruct this state on the client for the DOM to update properly.
Hydration’s basic approach is to execute the same code that generated the application state on the server again within the browser, which adds to execution time and delays interactivity.
That’s why many meta-frameworks serialize the application state on the server and include it in the HTML so that state can be restored using JSON.parse(). Deserialization is significantly faster than reconstructing state by executing application code on the browser, and it works well for simple and complex pages.
Even when application state is serialized, however, hydration still reconstructs internal framework state by slowly executing code.
For the final piece, frameworks need to recreate the component hierarchy, which is part of a framework’s internal state. It keeps track of which components need to be rerendered when your application state changes.
Similar to how it associates event handlers with DOM elements, hydration must download all of a page’s components and execute their templates to rebuild a component hierarchy, adding still more overhead.
Front-end frameworks perform hydration to recover event handlers, application state, and the component hierarchy in order to make the page interactive.
Each step requires downloading and executing code, which is expensive. Code execution time in particular is proportional to your page’s complexity when using hydration. We could roughly model this limitation with an equation:
No matter how small your payload, hydration will always be a bottleneck.
One solution to this problem is to eliminate the need to execute any code to restore a page’s interactivity, which we can do through serialization. As mentioned above, many meta-frameworks already serialize application state. Why not serialize event handler associations and component hierarchies, as well?
Because it’s really hard!
Function closures, promises, and resource references, among other structures, are all difficult to serialize. So a framework needs to be designed with serializability and resumability in mind. These aren’t features that can easily be added to existing frameworks without large-scale breaking changes.
The biggest win of serializing page state into HTML is that making the page interactive doesn’t require downloading or executing any template code. The framework simply resumes the page.
The bottom line? Resumable frameworks would reduce execution cost to zero and incur a constant overhead regardless of page complexity.
We at Builder.io have created a resumable front-end framework, Qwik. Our goal is to bring every web page’s time to interactive to its absolute minimum.
While we got the ball rolling, Qwik is open to the community. Everyone who’s as passionate about web performance as we are is invited to try it out and contribute and comment.
Head over to Qwik’s repository to learn more, or try our starter:
npm init qwik@latest