DEV Community

Cover image for Conquering JavaScript Hydration
Ryan Carniato for This is Learning

Posted on

Conquering JavaScript Hydration

That is an ambitious title for an article. An ambitious goal in general. Hydration, the process of spreading JavaScript interactivity back into our applications after server rendering, has been regarded as the most challenging problem for JavaScript frameworks for the past several years.

For all the effort we've put into Server Rendering on the web, we still haven't found a universally good solve for balancing the developer costs with end user costs.

Regardless of how we optimize for server rendering, hydration hangs over us. That JavaScript that needs to be run on page initialization, that makes our First Contentful Paints deceptive, that adds first input delay no matter how much we progressively enhance, and only gets worse the larger or more complex our web applications become.

Many have worked on the problem, contributing to various projects, all hitting different tradeoffs. Through them, we've seen the pieces of the puzzle come together. To that end, we are nearing a point where we can consider Hydration a solved problem.


Finding Resumability

It was March 2021. We'd been staring at how to solve async data fetching for the next version of Marko for months but had decided to move on. We had already implemented most of our cross template analysis, a mechanism to generate metadata for each module, that any parent could use to understand exactly how what is passed to it would be used. Our handcrafted benchmarks showed the approach was very performant. It was time to just build the compilation.

But Michael Rawlings(@mlrawlings) couldn't get past this sinking doubt that we were doing the wrong thing. Not wanting to rely on caches to prevent unnecessary data fetching during hydration he proposed we just not. Not re-run any components. Not execute any reactive expressions we already ran on the server. But doing that was not simple.

The initial answer came from Svelte. Svelte components slot all state into a hoisted scope and sort all expressions into appropriate lifecycles to avoid needing a reactive runtime.

So why not take that further if we can analyze across templates? As demonstrated by Solid when components no longer are the unit of change we can unlock incredible performance. And the benefits of breaking down this work for hydration may be even more pronounced.

As long as this scope is globally available, then we can break apart our components into many pieces without them being tied together by closures. Every piece is independently tree-shakeable and executable. All we need to do is serialize this scope from the server as we render, and register any browser-only code to run immediately on hydration.

I wrote about this journey in more detail in

As it turns out we weren't the only ones to arrive at a similar conclusion. Within a couple of months, Misko Hevery(@mhevery), creator of Angular, revealed this approach to the world in his framework Qwik. And he'd done something better than us. He'd given the idea a name.

Resumability.


Eliminating Hydration?

Image description

Fast forward to March 6th, 2022. Both projects have been working in this direction for about a year now. I was tasked that week with adding the <effect> tag to Marko 6. Yes, everyone's favorite hook.

Effects are fun as they live in userland and they have this quirky behavior in that they only run in the browser, as they are your opportunity to interact with the DOM. And you tend to want them to run after everything else which means inevitably some secondary queue that needs to run.

You could use JSDOM on the server but I'd recommend against that. Severely slows down server rendering speed to be working with an emulated DOM when you could just be using strings (How We Wrote the Fastest JavaScript UI Framework, Again).

So sitting there Monday morning in a meeting, we are agonizing about adding more runtime to handle the scheduling, when Dylan Piercey asks the obvious question.

Does anything other than effects need to run in the browser at hydration time?

We have event registration but it didn't do much as the events are all delegated to global handlers. Couldn't we just skip creating a hydrate export on any template that didn't run effects? If the end-user didn't register any effects at all do we need to run anything beyond a small script to bootstrap these global events?

While he and Michael continued working through the trade-offs of what it would mean for the compilation, I moved on to doing some performance benchmarks for various reactive queuing mechanisms where we'd noticed a bottleneck.

Misko sends me this message:
Image description

The timing was impeccable.

And he's completely right. Some people might want to argue the details. And it is justified. But it is more or less splitting hairs on definitions. We'd all been staring at these problems for a year now and somehow had completely missed the headline:


Hydration is a Solved Problem

Image description

There are details here that need some ironing out. But it has gotten to a point where there is a clear path to only running browser-only code in the browser at hydration time. Nothing beyond a simple bootstrap to load global event handlers needs to run. No re-running of components. No component-specific code is required to be executed otherwise. Just "resuming" where the server left off.

This covers the execution part of the story. There is still the problem of data serialization, as resumability has the potential to increase it. The solution Marko is developing leverages the reactive graph, along with the knowledge that the root of the page is only rendered on the server, to automatically detect what data needs to be serialized.

Resumability is also independent of when we load the code in the browser. Qwik has been developing a granular means to progressively load only the code required on each interaction. The intention is that usage analytics could be leveraged to optimally bundle in the future.

So there are going to be differences between different solutions. And details to reconcile. But the bottom line is we've seen 2 approaches to this now, and there will be more in the future.

This is just the starting line. With hydration potentially a thing of the past, the next generation of web development starts now.


If you want to see what it's about today check out Qwik. It uses JSX and reactive primitives to make developing performant apps easy. Here is my recent interview with Misko:

If you want to see what I've been working on, you will need to wait a bit longer. We are looking forward to releasing our first version this summer when Marko 6 goes into beta.

Top comments (11)

Collapse
 
feldev profile image
Félix Paradis

Very nice innovations!

Is Marko built on top of Qwik?
Or are they 2 completely separate frameworks with similar ideas?

Collapse
 
ryansolid profile image
Ryan Carniato • Edited

Two completely separate projects with similar ideas.

Marko was created and open sourced at eBay in 2014 and powers ebay.com. It has been pioneering the load performance optimized JavaScript space leveraging smart compilation for most of the last decade. Marko was the first open source JS framework to support automatic Partial Hydration(Islands) and Out of Order Streaming and has continued to innovate this space since then.

Qwik is the result of observations Misko Hevery made while working on Angular. He'd formalized it and presented the ideas back at a conference in 2019, but realized that Angular wasn't going to be able to support this vision. He left Google in 2021 and joined Builder.io to develop this new framework.

Collapse
 
fliponeup profile image
Oscar Lito M Pablo

Great article, Ryan, thanks, and which prompted me to pause and ask again a very fundamental question as to which framework to use for a biz startup web app now about to approach Minimum Viable Product development. My question to you is, if you were to decide which technology/framework to use, which one would it be -- Sveltekit, Qwick/Partytown, Marko, Astro -- given the following context/specs:

  1. The app will be a PWA
  2. Must work offline ("offline first")
  3. Must work under poor and intermittent Internet connection conditions
  4. Must have the smallest app (bundle) size possible
  5. Must be able to connect to and do CRUD operations with a cloud database (Firebase or Supabase)
  6. Must be fast

If it's Marko, great! But if Marko can't meet all the above requirements, which framework would you choose if this is your project?

Would love to get your take on this.

Thread Thread
 
pyrsmk profile image
Aurélien Delogu • Edited

I would add:

  • Must be easily maintainable
  • Must not have a too big learning curve
  • The project must still be alive in 5 years

😛

Thread Thread
 
pyrsmk profile image
Aurélien Delogu

So... What did you choose?

Collapse
 
lili21 profile image
li.li

I learned a lot about SSR and hydration from your articles and videos. Thanks a lot.
what resources( article, video, source code) would you recommend for learning the implementation detail about hydration? like from scratch.

Collapse
 
mathieuhuot profile image
Mathieu Huot

Quite interesting as usual. And a very deep subject indeed. Still trying to wrap my head around these "new" concepts. For example, could we say that resumability is like using something like petite-vue with a server side framework like say Express where petite-vue resumes what has been rendered on the server? Just thinking outload.

Collapse
 
ryansolid profile image
Ryan Carniato

Most of these frameworks that add arbitrary bits of JS on top of HTML still need to "render" once to setup their reactive subscriptions as they are determined at runtime. Qwik actually does determine them at runtime but with the server acting as the one time it runs, and then serializes the subscriptions into the page. Marko uses compiler analysis to know the dependencies and serializes that.

But most of the motivation here is to provide a single app experience when authoring. Ultimately what happens is we let the user basically write a full JavaScript application. Picture like writing a React app. And then through build process we break it into the little pieces that need to be sent to the browser, and serialize enough data so barely anything actually runs on page load. From there using fine-grained reactivity (like what petite-vue or Solid has) we just update the parts that change, as needed on end user interaction.

Collapse
 
noamr profile image
Noam Rosenthal

There's an assumption with the concept of resumability (and from what I read in this article) that perhaps I fail to understand. The assumption is that the app's code is nicely separated into components, and that each component's code is responsible for handling the events etc.

From my experience with larger apps, most of the code is actually a bunch of utils, common hooks, business logic, code that is not cleanly divisible to components.

In code bases like that, wouldn't it be so that resuming the app when the event happens leads to a big download and execution of those common dependencies? Wouldn't it be so that unless you really carefully craft your app to be separated to components and clean dependencies, what you did with resumability is postpone the long download and INP to after the first interaction that requires all those dependencies, perhaps separating out some component code at the fringe?

Maybe Marko/Qwik came up with solutions for this, would love to hear!

Collapse
 
ryansolid profile image
Ryan Carniato

Their are a couple pieces here. Most of these solutions were born out places where heavy business logic was preferred on the server anyway. Generally right now we are seeing this applied to MPAs which means that a lot of the heavy lifting is server side. We aren't necessarily talking admin dashboards, but eCommerce sites and like Google search.

I also don't tend to combine the download portion with the resumability. Qwik often does talk about that, but I think that distracts. Marko actually doesn't have that fine grained lazy loading mechanism. We're still of the mind that that sort of loading is still best served in larger parts related to defined/known feature sets and evaluating and making decisions rather than just breaking everything apart. So I wouldn't focus on that.

The important part is not hydrating when the page load. You can load all the JavaScript upfront, it isn't going to execute at that time anyway. Obviously some stuff being lazy is valuable but we don't have to necessarily wait until someone clicks.

The key to resumability is that the first interaction is the same without running a bunch of code as it would have been if you did all that hydration work. You aren't deferring work as much as eliminating a stage. It's a bit like trading CPU for memory. We serialize more to run less.

But back to the beginning if all the logic is needed in the browser then yeah there isn't much for that. We've seen in practice with Marko we can eliminate more unnecessary code than you'd expect making even fully interactive demos smaller than their typical client counterparts but the tradeoff isn't in INP or interactivity but rather server rendering time and network bandwidth. However using knowledge of what can or can't change actually counters that.

If part of the page never needs to update it isn't just the JavaScript you can save on but the related serialized data, both internals needed for Resumability, and end user data stores they would have otherwise needed to be present to properly hydrate. We get to just skip on all of that.

Of course with anything mileage will vary. If a system's business logic is one big tightly couple unit then maybe you are in for a bit of a hit anyway. But even in that case there is still a lot of opportunity here to reduce code and improve page startup.

Collapse
 
noamr profile image
Noam Rosenthal

Thanks, I think I understand the premise better.
In a nutshell, seems like resumability doesn't solve the issue of hydration as a web-development problem (e.g. how web-component code attach to custom HTML elements in the markup), but rather the hydration problem as presented by frameworks, because they have to generate all this internal data (e.g. VDOM in React). This is of course a valuable thing to solve.

Thread Thread
 
ryansolid profile image
Ryan Carniato

Yeah. More or less. Keep in mind most people author webcomponents with frameworks (like Lit or Stencil) so it is more of the same here too. But fundamentally this is a cost due to how we model the UI declaratively and need to get back to that when the app wakes up.

Collapse
 
joshuaamaju profile image
Joshua Amaju

I guess this is where event delegation comes in, just imagine downloading 100+ listeners to over a 100 list items. And do event listeners get downloaded every time?

Collapse
 
ryansolid profile image
Ryan Carniato

Event delegation is the key to not running over the component tree on client side startup. It still additionally requires being able to hoist out all the event handlers in a way they can be registered top level without running any component code which can be complicated since these tend to close over state.

It also requires writing some unique identifier/scope information into the DOM elements themselves at SSR time to know where to find the code and serialized state.

But if you can compile it that way ahead of time, when the browser starts up we only have to add a global delegated event handler for each type of event. No need to run the components. It's only when someone triggers the event that we'd look at the event target and walk up finding the appropriate handlers and scope based on what was written into the HTML that we execute application code.

Collapse
 
bigbott profile image
bigbott

All those complications is the result of applying SPA frameworks to what they are not applicable.

Just create traditional multi page applications, use Web Components for encapsulation and reuse and be happy.

Collapse
 
ryansolid profile image
Ryan Carniato • Edited

If only it were that simple across the board. The frameworks highlighted in this article aren't actually SPA frameworks. And using frameworks at all is overkill for certain things.

Server rendering + hydrating Web Components is its own thing. Lit has been adding support for this but more of the same, trading one framework for another. It does serve as a good basis for Island architecture similar to Astro. But as you scale for certain types web applications hydration comes back to haunt you.

While complex what is cool about the approach discussed here is it does reduce the startup execution cost. Yes writing pure vanilla can do this as well, but then SSR gets trickier as relying on emulated DOM on the server is recipe for poor performance.