Original: https://www.builder.io/blog/hydration-is-pure-overhead
Hydration is a solution to add interactivity to server-rendered HTML. This is how Wikipedia defines hydration:
In web development, hydration or rehydration is a technique in which client-side JavaScript converts a static HTML web page, delivered either through static hosting or server-side rendering, into a dynamic web page by attaching event handlers to the HTML elements.
The above definition talks about hydration in terms of attaching event handlers to the static HTML. However, attaching event handlers to the DOM is not the challenging or expensive part of the hydration, and so it misses the point of why anyone would call hydration an overhead. For this article, overhead is work that can be avoided and still leads to the same end result. If it can be removed and the result is the same, it is an overhead.
Digging deeper into hydration
The hard part of hydration is knowing WHAT
event handlers we need and WHERE
they need to be attached.
-
WHAT
: The event handler is a closure that contains the behavior of the event handler. It is what should happen if a user triggers this event. -
WHERE
: The location of the DOM element where theWHAT
needs to be attached to (includes the event type.)
The added complication is that WHAT
is a closure that closes over APP_STATE
and FRAMEWORK_STATE
:
-
APP_STATE
: the state of the application.APP_STATE
is what most people think of as the state. WithoutAPP_STATE
, your application has nothing dynamic to show to the user. -
FRAMEWORK_STATE
: the internal state of the framework. WithoutFRAMEWORK_STATE
, the framework does not know which DOM nodes to update or when the framework should update them. Examples are component-tree, and references to render functions.
So how do we recover WHAT
(APP_STATE
+ FRAMEWORK_STATE
) and WHERE
? By downloading and executing the components currently in the HTML. The download and execution of rendered components in HTML is the expensive part.
In other words, hydration is a hack to recover the APP_STATE
and FRAMEWORK_STATE
by eagerly executing the app code in the browser and involves:
- downloading component code
- executing component code
- recovering the
WHAT
(APP_STATE
andFRAMEWORK_STATE
) andWHERE
to get event handler closure - attaching
WHAT
(the event handler closure) toWHERE
(a DOM element)
Let's call the first three steps the RECOVERY
phase. RECOVERY
is when the framework is trying to rebuild the application. The rebuild is expensive because it requires downloading and executing the application code.
RECOVERY
is directly proportional to the complexity of the page being hydrated and can easily take 10 seconds on a mobile device. Since RECOVERY
is the expensive part, most applications have a sub-optimal startup performance, especially on mobile.
RECOVERY
is also pure overhead. Overhead is work done that does not directly provide value. In the context of hydration, RECOVERY
is overhead because it rebuilds information that the server already gathered as part of SSR/SSG. Instead of sending the information to the client, the information was discarded. As a result, the client must perform expensive RECOVERY
to rebuild what the server already had. If only the server had serialized the information and sent it to the client along with HTML, the RECOVERY
could have been avoided. The serialized information would save the client from eagerly downloading and executing all of the components in the HTML.
The re-execution of code on the client that the server already executed as part of SSR/SSG is what makes hydration pure overhead: that is, a duplication of work by the client that the server already did. The framework could have avoided the cost by transferring information from the server to the client, but instead, it threw the information away.
In summary, hydration is recovering event handlers by downloading and re-executing all components in the SSR/SSG-rendered HTML. The site is sent to the client twice, once as HTML, and again as JavaScript. Additionally, the framework must eagerly execute the JavaScript to recover WHAT
, WHERE
, APP_STATE
, and FRAMEWORK_STATE
. All this work just to retrieve something the server already had but discarded!!
To appreciate why hydration forces duplication of work on the client, let's look at an example with a few simple components.
We'll use a popular syntax understood by many people, but keep in mind that this is a general problem not specific to any one framework.
export const Main = () => <>
<Greeter />
<Counter value={10}/>
</>
export const Greeter = () => {
return (
<button onClick={() => alert('Hello World!'))}>
Greet
</button>
)
}
export const Counter = (props: { value: number }) => {
const store = useStore({ count: props.number || 0 });
return (
<button onClick={() => store.count++)}>
{count}
</button>
)
}
The above will result in this HTML after the SSR/SSG:
<button>Greet</button>
<button>10</button>
The HTML carries no indication of where event handlers or component boundaries are. The resulting HTML does not contain WHAT
(APP_STATE
, FRAMEWORK_STATE
) or WHERE
. The information existed when the server generated the HTML, but the server did not serialize it. The only thing the client can do to make the application interactive is to recover the information by downloading and executing the code. We do this to recover the event handler closures that close over the state.
The point here is that the code must be downloaded and executed before any event handler can be attached and events processed. The code execution instantiates the components and recreates the state (WHAT
(APP_STATE
, FRAMEWORK_STATE
) and WHERE
).
Once hydration completes, the application can run. Clicking on the buttons will update the UI as expected.
Resumability: a no-overhead alternative to hydration
So how do you design a system without hydration and therefore, without the overhead?
To remove overhead, the framework must not only avoid RECOVERY
but also step four from above. Step four is attaching the WHAT
to WHERE
, and it's a cost that can be avoided.
To avoid this cost, you need three things:
- Serialize all of the required information as part of the HTML. The serialized information needs to include
WHAT
,WHERE
,APP_STATE
, andFRAMEWORK_STATE
. - A global event handler that relies on event bubbling to intercept all events. The event handler needs to be global so that we are not forced to eagerly register all events individually on specific DOM elements.
- A factory function that can lazily recover the event handler (the
WHAT
).
A factory function is the key! Hydration creates the WHAT
eagerly because it needs the WHAT
to attach it to WHERE
. Instead, we can avoid doing unnecessary work by creating the WHAT
lazily as a response to a user event.
The above setup is resumable because it can resume the execution where the server left off without redoing any work that the server already did. More importantly, the setup has no overhead because all of the work is necessary and none of the work is redoing what the server already did.
A good way to think about the difference is by looking at push and pull systems.
- Push (hydration): Eagerly download and execute code to eagerly register the event handlers, just in case of user interaction.
- Pull (resumability): Do nothing, wait for a user to trigger an event, then lazily create the handler to process the event.
In hydration, the event handler creation happens before the event is triggered and is therefore eager. Hydration also requires that all possible event handlers be created and registered, just in case the user triggers the event (potentially unnecessary work). So event handler creation is speculative. It is extra work that may not be needed. (The event handler is also created by redoing the same work that the server has already done; hence it is overhead)
In a resumable system, the event handler creation is lazy. Therefore, the creation happens after the event is triggered and is strictly on an as-needed basis. The framework creates the event handler by deserializing it, and thus the client does not redo any work that the server already did.
The lazy creation of event handlers is how Qwik works, which allows it to create speedy application startup times.
Resumability requires that we serialize WHAT
(APP_STATE
, FRAMEWORK_STATE
) and WHERE
. A resumable system may generate the following HTML as a possible solution to store WHAT
(APP_STATE
, FRAMEWORK_STATE
) and WHERE
. The exact details are not important, only that all of the information is present.
<div q:host>
<div q:host>
<button on:click="./chunk-a.js#greet">Greet</button>
</div>
<div q:host>
<button q:obj="1" on:click="./chunk-b.js#count[0]">10</button>
</div>
</div>
<script>/* code that sets up global listeners */</script>
<script type="text/qwik">/* JSON representing APP_STATE, FRAMEWORK_STATE */</script>
When the above HTML loads in the browser, it will immediately execute the inlined script that sets up the global listener. The application is ready to accept events, but the browser has not executed any application code. This is as close to zero-JS as you can get.
The HTML contains the WHERE
encoded as attributes on the element. When the user triggers an event, the framework can use the information in the DOM to lazily create the event handler. The creation involves the lazy deserializing ofAPP_STATE
and FRAMEWORK_STATE
to complete the WHAT
. Once the framework lazily creates the event handler, the event handler can process the event. Notice that the client is not redoing any work that the server has already done.
A note on memory usage
The DOM elements retain the event handlers for the lifetime of the element. Hydration eagerly creates all of the listeners. Therefore hydration requires allocating a memory on startup.
Resumable frameworks do not create the event handlers until after the event is triggered. Therefore, resumable frameworks will consume less memory than hydration. Furthermore, the resumable approach does not retain the event handler after execution. The event handler is released after its execution, returning the memory.
In a way releasing the memory is the opposite of hydration. It is as if the framework lazily hydrates a specific WHAT
, executes it, and then dehydrates it. There is not much difference between the first and nth execution of the handler. Event handlers' lazy creation and release does not fit the hydration mental model.
Conclusion
Hydration is overhead because it duplicates work. The server builds up the WHERE
and WHAT
(APP_STATE
and FRAMEWORK_STATE
), but the information is discarded instead of being serialized for the client. The client then receives HTML that does not have sufficient information to rebuild the application. The lack of information forces the client to eagerly download the application and execute it to recover the WHERE
and WHAT
(APP_STATE
and FRAMEWORK_STATE
).
An alternative approach is resumability. Resumability focuses on transferring all of the information from the server to the client. The information contains WHERE
and WHAT
(APP_STATE
and FRAMEWORK_STATE
). The additional information allows the client to reason about the application without downloading the application code eagerly. Only a user interaction forces the client to download code to handle that specific interaction. The client is not duplicating any work from the server; therefore, there is no overhead.
To put this idea into practice, we built Qwik, a framework that is designed around resumabilty and achieves excellent startup performance. We're also excited to hear from you! Let's keep the conversations going and get better as a community at building faster web applications for our users.
— Love from the Builder.io team.
PS: Many thanks to Ryan Carniato, Rich Harris, Alex Patterson, Dylan Piercey, Alex Russell, Steve Sewell who provided constructive feedback for the article. ❤️
We know you have a few burning questions. That's why we've put together an FAQ to address them all.
Why coin a new term?
Resumability does not have a clear line indicating that component X is or is not hydrated. If you insist on saying that Qwik hydrates, then you have two choices:
- Qwik apps are hydrated when the global event handler is registered. It does not feel right because no app code has been downloaded, and no work has been performed.
- Qwik apps are hydrated when the first interaction parses the serialized state. Hydration is attaching event listeners for interactivity. Deserializing state is there to recover the application's state and has nothing to do with registering event handlers. There are situations where the deserialization does not need to happen, and when it does, it happens after the event has been triggered. The other problem is that deserialization of state restores state even for components that have not been or will never be downloaded. So while it's tempting to assume that this is the point of hydration, we think of it as just lazy deserialization of app state as it does not directly relate to event processing.
Neither of these options feels satisfactory, and so we're coining a new term. You could define hydration as making the app interactive, but such a definition is so broad that it will apply to everyone, which will make it less valuable. So while it may sound like we're splitting hairs, we like to talk about hydration vs. resumability as we believe it better captures the massive difference in the amount of work required to make the application interactive.
Is resumability just hydration after the event?
It's certainly a valid way to look at it. However, there's one big difference. Resumability does not require that the framework download and execute the components to learn about the component hierarchy. Resumability requires that all of the framework's information be serialized in the HTML, including:
- Location of the event listeners and event types.
- Where to download the event
- Component boundaries
- Component props
- Projection/children
- Where to download the component re-render functions if needed
Since the framework deserializes all of this information and continues the execution where the server left off, resumable is a much better word.
What are real world results like?** **Where can I see a site that uses a resumable strategy?
Builder.io used the resumable strategy (and Qwik) to redo our website. We’ve removed 99% of JavaScript from startup and the resulting app feels super snappy even on mobile. Using Qwik and Partytown, we were able to cut down 99% of the JavaScript in our site and get a PageSpeed score of 100/100. (You can still visit the old page using hydration [PageSpeed 50/100] and compare it to the new page using resumability [PageSpeed 100/100] to experience the performance difference for yourself.)
Qwik's documentation is running on Qwik. You can look under the hood by opening up the dev tools (incognito) in your browser and notice the lack of Javascript on startup. (The page also uses Partytown to move third-party analytics to web workers.)
Finally, check out a to-do app demo running on Cloudflare edge. This page is ready for interaction in about 50 ms!
My framework knows how to do progressive and/or lazy hydration. Is that the same thing?
No, because progressive/lazy hydration still can't continue where the server left off. All component code needs to be downloaded and executed to recover and install the event handlers.
With resumability, many components will never download because they never change. But these components can pass props to child components or create content that the child component projects. So even if not interactive, the components' state needs to be recovered by re-execution. Recovering props is why islands that enable progressive/lazy hydration can't be arbitrarily small.
Simply put, resumability requires significantly less code to download and execute to handle a user interaction than hydration.
My framework knows how to create islands. Is that the same thing?
The island architecture breaks up the application into islands. Each island can then be hydrated independently. Instead of one big hydration, the total amount of work is now spread over many smaller hydration events. Typically, a trigger causes the island to hydrate lazily instead of eagerly on startup.
This means island-based hydration is an improvement as it can break the work down into smaller chunks and delay its execution, but it's still hydration and not the same as resumability.
My framework knows how to serialize the state. Does it have overhead?
The issue here is that the word state is overloaded. Yes, there are some meta frameworks that can serialize the state. But here, "state" means APP_STATE, not FRAMEWORK_STATE. I'm unaware of any popular framework (or meta-framework) that can serialize FRAMEWORK_STATE. Additionally, even if FRAMEWORK_STATE was serialized, the WHAT and WHERE are not.
Yes, serialization of state (APP_STATE) is useful and avoids a lot of work on the client. But it still results in hydration.
Is a component hydrated on first interaction?
If you look at the internal state of a resumable framework, you'll find no difference between the first interaction of the component and subsequent interactions. The only difference is that the framework has parsed the serialized state. And once the state is parsed, it applies to all components, not just the one the user has interacted with. At any point, the framework can serialize the state back to the HTML. Does that mean that the application is no longer hydrated? From that point of view, deserialization of state hydrates all components, even if their code has not been downloaded.
How can I take advantage of resumability today?
The framework controls the kind of recovery strategies it uses to make the application interactive. Therefore, to take advantage of resumability, your application needs to be using one of the frameworks which support it. Currently, we are only aware of Qwik which aims explicitly for resumability. The benefits of resumability are too great to be ignored, so I'm sure that other frameworks will start using this strategy in the future, whether they're new frameworks or existing frameworks that choose to migrate to resumability.
Is there a delay on first interaction?
Not when you use prefetching. Qwik is unopinionated about prefetching - we have used a variety of strategies for prefetching (eager, on visible, analytics-driven) to great results. In most cases, we find prefetching in a Web Worker, such as with Partytown, leads to the best balance of 0 cost and high speed. We will bake best practices into the framework and provide examples with recommended patterns over time.
Can I use my React/Angular/Vue/Svelte components with Qwik?
Rewriting an application is a huge undertaking. To lower the barrier to entry, we are looking into creating interoperability with some of today's more popular frameworks. Qwik will be your orchestrator delivering instant applications startup, but it will still work with most of the existing code investment. Think of Qwik can as the orchestrator for your current application. This way, you don't have to rewrite the whole application but still get some benefits. This is a work in progress, so stay tuned.
And if you are curious what is coming up next for Qwik:
If you made it this far, thanks for reading! Learn more about Qwik here, or follow us on Twitter to get updates as we have them
Top comments (2)
Back when I used SSR to generate my dynamic markup, I would have a global set of event handlers for scroll, move, click, submit, etc, then use data attributes to declaratively identity what effect i wanted. For example: a href=“somepage.php” data-delegate=“ajax” data-target=“#someexistingcontainer”. That worked really well, because even after loading or replacing content via AJAX i still had the event registered on the body or document. The problem is, I had my code in two places: one for the PHP template for the server-side component, and another for the event handler registration.
One thing I like about CSR is that I can have the template of the component and the event handlers defined all in the same file, which makes it a lot easier to keep track of and maintain. How does Qwik deal with that?
If you click on the button, the internet is slow, it starts downloading the JS. I click the button again because I think nothing happened, and again. So 3 clicks are stored. The JS now comes back, does it fire the JS method that came back 3 times?