DEV Community

Cover image for Why Efficient Hydration in JavaScript Frameworks is so Challenging

Why Efficient Hydration in JavaScript Frameworks is so Challenging

Ryan Carniato on February 03, 2022

Hydration is the name given to the process in JavaScript frameworks to initializing the page in the browser after it has previously been server ren...
Collapse
 
peerreynders profile image
peerreynders

Whatever happens with Qwik in the long term, it added important new terminology—"replayable SSR" versus "resumable SSR".

I would go as far as saying that it identified the core limitation of contemporary client side frameworks—SSR reliant on "replayability" which is a consequence of their client side centric design (as opposed to a design that is distributed from the beginning).

Dynamic server rendering pretty much follows this pattern:

  • Gather the necessary dynamic data.
  • Render that page state into HTML and ship it of to the client (the template may or may not include JS for interaction)
  • After parsing the payload from the server the page is ready to go.

This pattern is pretty much guided by doing as much work as possible on the server to minimize the work needed on the client (and taking maximum advantage of the browser's built-in capabilities) which makes sense in a distributed environment where

  • connection quality is unknown
  • client computational power is unknown

Extending this to a distributed application leads to resumable server rendering:

  • Gather the necessary dynamic data.
  • Render the application's initial state.
  • Transform that application state into HTML and ship both to the client.
  • The client displays the HTML as is but joins the delivered initial application state into the application which is then in control of interaction.

The key here is that the "pre-rendered initial application state" isn't just "initial data" that needs to be "played out" inside the application but is the actual internal application state—so the application is ready to go once it's dropped in. Not only is the display shipped as pre-rendered HTML but also the application state is pre-rendered.

Now Qwik adds some other tricks like fragmenting interactivity to the smallest possible granularity in order to aggressively optimize TTI but the fundamental shift in thinking is moving from today's "replayable SSR" to tommorrow's "resumable SSR" (pre-rendered, joinable application state).

It's my sense that "resumable SSR" is going to force a major rethink in terms of client side application architecture (how it's partitioned) which may require significant tooling to get to the necessary level of DX that would ease its adoption.

If you have derived data trying to get back to the original to re-derive is in many cases impossible.

If I remember correctly Qwik initially was trying to store application state in the HTML but I think they have abandoned that idea.

Collapse
 
ryansolid profile image
Ryan Carniato • Edited

Yeah the reason I didn't focus too much on resumable hydration here is I don't think true resumable hydration exists yet. Qwik does succeed at doing out of order hydration in that you can hydrate the child before the parent. And with that partitioning it is possible to assume that data initializes with initial state from the server at a component level. You have the inputs and you have the outputs, so you can confidently just not do the re-render of that component in the browser at hydration time and only attach the event handlers, or any defined side effects.

However, the reason I don't consider this true resumable hydration is that when any data would update for this component it needs to re-run and redo unrelated work that may have already been done on the server. It's like the motivation for the React forget compiler example if you saw that.. if you have some state that has nothing to do with a list you are rendering and you update the state the list re-renders. This is unsurprising. Picture if that unrelated work triggered an async data request etc..

VDOM based solutions have some challenges with resumable hydration. Qwik's approach is more like skipping hydration and then doing the work when you interact with it. Resumable Hydration should be able to work not only skipping unnecessary hydration but be able to not redo computations and derivations in the browser after the fact when unrelated data changes. This is possible if you can slice up your component logic the other way more similar to the fine-grained graphs that Solid produces. And you are probably unsurprised to hear this is exactly what we are working on for Marko 6.

I am playing with Qwik right now and seeing just how effective their resumable approach is, because I do want to confirm my understanding is correct.

UPDATE: Talking to Misko and the team Qwik is already working on a solution for precisely the concern that I have. They have a runtime based reactive system similar to Solid and are serializing the dependencies at runtime on the server with some new primitives that resemble useEffect and useMemo. When this lands they should be able to do the type of fine-grained resumability I'm talking about.

Collapse
 
mhevery profile image
Miško Hevery

I had a most excellent chat with Ryan offline and wanted to share my thoughts here, which boil down to "Qwik is truely resumable." Maybe not right this moment, as we have a few more things to implement, but we have designs for all of these.

To be truly resumable, you need to have a few things:

  1. As Ryan pointed out, you need to attach to existing components out of order.
  2. You need to serialize the whole state of the application.
  3. You need to understand which component knows about what state. (Subscriptions)

If you don't have #3, then once the data changes, you don't have a choice but to re-render the whole app because you don't know which components are invalidated and need to be re-rendered. This forces you to download the whole app, and that is expensive.

But Qwik does know the answer to #3. Though Proxys Qwik knows which component cares about which data, Qwik serializes all of this information to the DOM. Then the data gets mutated Qwik can run querySelectorAll on HTML, which will tell it which components are subscribed to the data and hence have to be re-rendered. This is significant because it allows most of the components to stay in their unhydrated state for the duration of the app's life.

I think of this problem as layers in onion:

  1. Most obvious is how to serialize listeners. (If you can't do that, than you have to rehydrate)
  2. How do I render components out of order (If you can't do that that you will be forced to render parent/children)
  3. Know which component is invalidated on state change (If you can't do that, you will be forced to re-render the whole app)
  4. Have ways to watch data without running the code (useEffect equivalent) (If you don't have that you will have to eagerly download code)
  5. Have ways to deal with data that is not Serializable. (if you can't do that, than the kinds of apps you can build will be limited)

On each of the levels, there are interesting problems to solve. Qwik's aim is to solve all of them, and so far, we have been able to do that and have plans for the reminder, which is consistent with Qwik's mental model.

This is why I think that Qwik is (will be) truly resumable.

Thread Thread
 
btakita profile image
Brian Takita • Edited

I appreciate both yours & Ryan's work so this thread is gold. Thank you for listing out the hydration scenarios. Here is my concern as an app & library developer. I really like isomorphic JS & am building around that concept. Up till now isojs hydration has been a black box. Making the isojs hydration process more transparent & even programmable will be appreciated.

I would like to learn more about your plans re: step 4 & 5. Also, do you forsee Qwik or its concepts as being compatible with Solid & other isojs component libraries?

Thread Thread
 
ryansolid profile image
Ryan Carniato

I love Misko's framing here as 4/5 are big part of what we are working on with Marko as well. 4 is a fairly natural extension of 3 in the same way Solid's granular reactivity is over say MobX + VDOM. The way to do this involves serializing the dependencies, then you don't need to run it once. In Marko's case it is a compiler that detects these similar to Svelte, except Marko's works cross file and traces dependencies even hoisted. For Qwik it's runtime similar to Solid, just that if it runs on the server you know the dependencies from the last run and can continue only when one updates. The way they serialize them is simply to take the key off their shared proxy store that all components use (that manages the Dependency injection).

I do want to talk to Misko more about this because there are different types of scenarios. Derivations definitely want to run on the server and be treated this way but effects in most frameworks are also a way to do client only code that you don't run on the server, like include a jQuery chart. In those cases you'd want the effect to run at hydration time so it isn't really a problem since you don't need it to run on the server to collect dependencies. In Marko we make that deliberation that it is effects and event handlers that run at hydration time. The interesting piece for Qwik is do they code split on this too putting the effects in with the event handlers and separate from the other component code.. once you follow this thinking Qwik may actually approach closer to how Marko 6 is breaking up code.

For Marko we actually split it from input/props through Reactive graph so a component consists of many different fine-grained pieces. Which is interesting since instead of things being component level the parent can know based on whether it passes static or reactive data to even import the dynamic child part to run it. And since we know exactly what can be stateful we can eliminate whole subtrees through the component structure.

I'm excited to talk more about this because the implications get pretty interesting. Because even when things are stateful instead of serializing everything we can optimize it further to serialize things based on the leaf nodes of the reactive graph. It's only things read from an DOM, event handler or effect directly that need to be serialized. With one except reactive convergences need each source branch serialized as well so one side can update without needing to run again. In a sense I've shown how Marko today can only serialize data that is used in components based on what is passed in to top level components input/props. With this approach we'd get field level serialization of only what is actually used/usable. Because of convergence we know that and in other cases we know that it would be impossible change what data we are seeing without refetching (ie triggering an async source) so we can be confident we don't need to have it in the browser initially.

Of course this all comes down to what is serializable. I'm interested to learn more about Qwiks approach. With Marko since the compiler sets the boundaries we can always step up one level with serialization if it isn't serializable as long as all root sources are. But we are working on techniques to serialize typically unserializable things like promises or functions and closures.

Collapse
 
brokenthorn profile image
Paul-Sebastian Manole

I think it just did, with Qwik 1.0 just having been released recently.

Collapse
 
peerreynders profile image
peerreynders

I don't think true resumable hydration exists yet.

Given your article's introduction the characterization of "replayable hydration" (for the current state of SSR) versus the hypothetical ideal of "resumable hydration" seemed useful.

Collapse
 
mhevery profile image
Miško Hevery

If I remember correctly Qwik initially was trying to store application state in the HTML but I think they have abandoned that idea.

No we have not abandoned it all. We have just moved it to the bottom of HTML for several reasons:

  • Sending state later allows UI to load faster
  • putting all state together allows us to serialize graphs (not just DAGs)
Collapse
 
peerreynders profile image
peerreynders

Sorry, I should have been more precise. Back in August

the state seemed to be stored directly in HTML element attributes.

By December it consolidated into a qwik/json script block containing "flattened JSON with reference keys" (with some reference keys being used as HTML element attribute values).

Thread Thread
 
mhevery profile image
Miško Hevery

Yes that is correct, we have moved it to a consolidated place.

Collapse
 
rodpatulski profile image
rodpatulski

I'm having trouble understanding the difference between 'initial state' vs 'actual internal application state'. If the point is to have the app take over once the initial app is rendered on the client, then isn't initial state, the only state there is? Can you give an example of a difference?

Collapse
 
peerreynders profile image
peerreynders

From Static to Interactive: Why Resumability is the Best Alternative to Hydration and Hydration is Pure Overhead get into this in detail.

Perhaps replace “initial state” with “initial data”.

Hydration:

  • Use initial data to “boot up” the application on the server to capture the resulting HTML.
  • Combine initial data into the HTML and ship it to the browser.
  • In the browser use the initial data to “boot up” the application again (replay) to create application and framework state.
  • Create the necessary bindings between the DOM (created from the HTML loaded by the browser) and the framework state.

Resumability:

  • Use initial data to “boot up” the application on the server to capture the resulting HTML, application state and framework state.
  • Serialize application and framework state and combine it with the HTML and ship it to the browser.
  • Initially in the browser there is only one tiny event handler for the entire page so the page is ready to go as soon as the HTML has finished parsing. There is no second “boot up”.
  • Upon interaction the event handler refers to the serialized framework state to download the required application code and “resume” that part of the application with the associated application and framework state.
  • Additional parts of the application are activated in the same manner only as needed.
Collapse
 
trusktr profile image
Joe Pea

It's probably worse since now it's a guarantee that you will be making them wait. But your site will have a better Lighthouse Score.

I love how you pointed out that lighthouse scores can be meaningless. UX can still suffer despite a perfect score.

Collapse
 
reywright profile image
Rey Wright

I'm assuming this article started from this thread? twitter.com/RyanCarniato/status/14...

Collapse
 
ryansolid profile image
Ryan Carniato

I can see why you'd think that. It isn't unrelated. I sort of knew that the Remix guys were going to attempt to downplay partial hydration again. It isn't the first time. Expect the same for Streaming, or any other technology that doesn't start and end with React before React 18. So this seems timely but it's more I was prepared.

I think the biggest thing here is to have the language to talk about the work people have been doing. Whether or not I can convince someone of the merits of techniques developed by companies working at the largest scale is besides the point. After all it might not be worth any of the tradeoffs for someone's given app. But best know what those are.

We have people working on solutions in this space for years and there are slight differences in how everyone attacks the problem. The best way to cut through this is at least have the frame of reference to talk about these things.

Collapse
 
ryanflorence profile image
Ryan Florence

This is a weird comment that assumes a lot about us?

I'm not trying to downplay anything. I'm genuinely interested in partial hydration, been anticipating doing it ourselves in Remix since the beginning. But we've gotten so far with progressive enhancement I'm now not sure there's much to improve, so I'm just looking for a well-designed demo that shows it beats PE before I go mess around with it.

Nobody has one, I hope they do soon.

Thread Thread
 
ryansolid profile image
Ryan Carniato • Edited

Thanks for your concerns Ryan. I'm a bit annoyed by the situation and I let that show. We can talk some more about this in private. I have nothing but love for Remix.

Collapse
 
redbar0n profile image
Magne • Edited

@ryansolid Here’s a crazy idea:

Progressive Eager Hydration - with JavaScript streaming and execution by way of HTML streaming.

The goal is to make partial pages interactive asap, so the user can interact above-the-fold immediately (while the rest of the page is also made interactive asap, without the disadvantage of laziness: surprise stall on user interaction).

Could JavaScript be streamed in, in parallel with the HTML, and then progressively but eagerly hydrated (top down) incrementally, as the HTML is streaming in?

I imagine rendering would could go in progressive lock-step (after the HTML and JS streams in parallel): render some HTML -> hydrate corresponding JS component -> render some more HTML -> hydrate corresponding JS component -> … etc.

The framework would need to inject intermittent script tags in the HTML, in a very finely grained manner. Since heavy operations inside a component would block the rendering. Devs would also immediately get aware of slowdowns and optimise above-the-fold components. All so that the JS can be executed during streaming before the entire page is loaded:

Run JS before all HTML has loaded

Maybe this would be more line with the original vision of the web (before we collectively decided to put all JS inside a single script tag at the bottom of the HTML).

Collapse
 
ryansolid profile image
Ryan Carniato

This is how Marko works with in order streaming. In a sense it is how Solid and React's out of order streaming works. Not necessarily splitting it above the fold, but rather based on async data. Now making it work above the fold isn't that far from how things like Astro work with intersection obvserver. I think eager generally is good if it isn't blocking. React has termed "Selective Hydration" as this approach.

All that being said Resumability may be just better than all of this because then the code never needs to run. I think the conversation with Qwik gets too often pulled into the lazy loading story. That isn't the important part for a lot of things. Hard to do less work than eliminating Hydration.

Collapse
 
redbar0n profile image
Magne

Maybe you could even offload Hydration to a separate thread (web worker) that runs in parallel with the HTML being streamed in.

github.com/BuilderIO/partytown

An initial script tag at the top of the page could set the JS framework off on a separate worker thread, and the framework would need to take in (or listen to) the HTML/DOM as it becomes available, and then hydrate it incrementally. Not sure how wise it is to rely on support for web workers, though.

Collapse
 
ryansolid profile image
Ryan Carniato

Maybe.. but I think we can just do better doing less work still. Party town is cool because what it does is super low priority, no one is waiting on it. But using a worker like this much slower for working with the DOM. Solutions are getting too fancy with deferring when we should just be focusing on doing less work. Resumable Hydration is great, Partial is great at mitigating it. Progressive has its usage but is very overrated.

Collapse
 
redbar0n profile image
Magne • Edited

FFR, adding a small code example with preview of the basis of this idea. When loading this HTML, the JS executes synchronously in between the rendering of each section of HTML:

<!DOCTYPE HTML>
<html>

<body>

  <p>Before the script...</p>

  <script>
    alert( 'Hello, world!' );
  </script>

  <p>...After the 1st script tag.</p>

  <script>
    alert( 'Hello, again!' );
  </script>

  <p>...After the 2nd script tag.</p>

  <script>
    alert( 'Hello, once more!' );
  </script>

  <p>...After the 3rd script tag.</p>

</body>

</html>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
redbar0n profile image
Magne • Edited

hey @adrienpoly @marcoroth this approach could maybe be used with StimulusJS to make the page interactive faster, while the HTML and JS are being streamed in. You could have Rails interleave Stimulus controllers in JS script tags in the HTML immediately after the HTML tags with the corresponding data-controller attribute (you could even mark the script tag async, to progressively load it from the server and it will «hydrate» and make the HTML interactive asap). So the user doesn’t have to wait until all the JS is downloaded to start interacting (above the fold, for instance). Not sure if this is what Rails with Stimulus already does though…

Collapse
 
shamsup profile image
Alex Mitchell

I think I'm missing the benefit in taking on the complexity of partial hydration when you still have dynamic parts that will need to access data from the server regardless of when they are hydrated. Fewer round-trips to the server is better, right? Is this complexity just to reduce the initial JavaScript size delivered to the client?

Are there examples in the wild of partial hydration in a complex app that can be used to compare against SSR + hydration, like we saw from remix with the ecommerce demos?

While all great ideas on paper, I think this is missing some substance to back up the claims against the tried and true champion: SSR.

Collapse
 
ryansolid profile image
Ryan Carniato

I do think you are missing something. The article might be worth a re-read. This is all SSR and doesn't introduce more round trips. If you mean that MPAs do a full page reload sure but keep in mind you are hitting the server already in these scenarios. But you are correct initial page load is a big part of it.

Unfortunately where you see this employed the most is huge eCommerce. Those sites are also plagued by 3rd party scripts and ads. We develop independent of them and since introducing Marko at ebay.com in 2013 we haven't been allowed to let any degradation in performance come from the numbers we doing in Java + jQuery before then.

It's not just ebay. Amazon has similar concerns:


In fact most large companies have a version of this when you consider internal framework Wiz from google and the investment React has been making into server components.

I can show some size comparisons from ebay.com between sizes of partial (component) and full hydration. It doesn't take much to do a little math to see the impact.
size

I know the hackernews demos are pretty poor in that they are mostly uninteractive so removing the script tags almost does the same thing. But even then you can already see the benefit. In fact in the original version of the Remix vs Next article Remix did exactly that so it isn't unlike the creators are naive to the benefits here.

But it goes beyond just code size but serialization savings. I just went on hackernews and found a story with an absurd amount of comments. Look at the difference between Marko and Remix anyway way you choose. I made essentially identical versions of this demo:

Remix: remix-hackernews.ryansolid.workers...
Marko: hn.markojs.workers.dev/story/30186326

Notice the difference streaming and partial hydration make here. I can keep churning out examples but it takes time.

Collapse
 
shamsup profile image
Alex Mitchell

This is much more clear to me now, thank you. The issue isn't SSR itself, the issue is the hydration strategy after server rendering. The first few paragraphs seem almost like an attack on SSR, so I was confused why you'd be discussing concepts that build on top of SSR.

You've got a 👍 from me.

Thread Thread
 
ryansolid profile image
Ryan Carniato

Yeah I felt it necessary to knock the "l'm doing SSR in my SPA so I'm good" mentality down a couple pegs in order to help reset expectations for broader look at the topic. To me this is hardly a done topic. There are so many ways to make it better.

So I'm really excited about all the technology in this area. Remix and Sveltekit improves on Next. We're all moving forward. I think its important to view our efforts at like a 2-3/10 right now instead of the 9/10 people are sort of picturing. I think this area is pushing our mental model of what the best architectures are and how they apply to different use cases. Honestly I'm not sure there is a clear direction with both things like React Server Components and Qwik showing us what is possible with really different models. Or how Marko continues to show that these techniques can be done automatically and more optimal than a human would practically write the code.

Collapse
 
kapouer profile image
Jérémy Lal • Edited

I implement rehydration by leveraging the different steps for building a document:

  • pathname
  • query
  • paint (when document is visible or view changes)
  • hash (when location hash changes)

When browser history changes, depending on what part in the location changes, following steps are replayed.
If the document becomes visible, paint phase is replayed.
If the document is hidden, paint/hash phases are not run, this is the SSR case (unless rendering to a PDF, in which case the document is "visible").

The built document just has to carry at which step it is sent to the client:

  • no step, the client must do all steps
  • query, the client resumes at paint, then hash

When browsing, a change in query just replays query/paint/hash, not pathname step.
Custom elements help a lot in this picture.

Collapse
 
btakita profile image
Brian Takita • Edited

Programmable hydration (e.g. jQuery, Alpine.js) is another strategy which has been used by MPA libraries like Ruby on Rails. The isomorphic rendering JS libraries eschewed this path but it may be worth revisiting now.

This would achieve programmable hydration & reduce payload size & reducing double data at the cost of imperatively managing the behavior injection. It could facilitate a custom Islands, Server Component, or lazy behavior injection implementation.

It may not be as ergonomic as Islands/Server Components but it can be improved with hydration libraries/hooks, compilation, & asset servers (Vite), tools which were not available or were only available in limited ways (ASP.NET) when the initial push to create isomorphic rendering libraries occurred.

I personally went all in on isomorphic rendering but perhaps behavior injection could be augmented with isomorphic rendering, compiled templates, & asset servers to take advantage of programmability in hydration while mitigating the complexity of the 2 layered app.

Collapse
 
lexlohr profile image
Alex Lohr

I think that there is no single ideal solution for different use cases and the future will show frameworks that allow developers to control the process in a sensible way to succeed where frameworks that try to take the cognitive load off the developers will fail in many cases.

A web shop will need a completely different hydration solution from that of a complex web app; unless the result is faster and more maintainable than hand crafted HTML with jQuery (e.g. amazon.com), it won't be winning.

A complex web app reliant on lots of JS on the other hand will likely rather have only its skeleton, noscript message and SEO data pre-rendered and rely on a mix of hydration and client-side rendering.

Control is key. If I can switch between different modes within the same root component just by dropping or removing a few flow components, and have conditions at hand to use in the state logic, that's a definite win; one could easily a/b-test different solutions for different use cases to get the maximum possible performance without spending days of work.

Collapse
 
lil5 profile image
Lucian I. Last

Maybe using Laravel + React but GoLang + AlpineJS beats JS hydration out of the water

Collapse
 
btakita profile image
Brian Takita

Are there any benchmarks comparing Go + AlpineJS vs PWA vs various JS library hydration solutions?

Collapse
 
ryansolid profile image
Ryan Carniato

Yeah depends on what we are testing. Like most of these test the speed of a single response to line it up with the hydration expectations. In those scenarios the response time between these backends is probably not going to make a huge difference because the cost of network and browser costs way outweigh a handful of milliseconds on the server. I wouldn't be surprised that a light JS layer on top of pre-rendered HTML would perform well.. Alpine itself I'm not sure as it often is larger than other dedicated JS Frameworks and benchmarks pretty poor on performance. But simply it being so granularly applied probably still makes a it a good choice over like a giant React app.

This isn't so much about whether you can just use some vanillaJS to add interactivity. You most definitely can. Mind you it's like layering 2 apps on top if each other. This is about looking at how you can build it all as a single app experience which I think has more interesting implications. Especially when trying to scale the complexity of these systems from simple site to interactive app all with a single development experience.

Collapse
 
lil5 profile image
Lucian I. Last

stressgrid.com/blog/webserver_benc...

Here is a benchmark of the underlying language servers

Thread Thread
 
btakita profile image
Brian Takita • Edited

It would be interesting to see a benchmark with the server & client TTL. AlpineJS does not score well on the Krausest benchmarks. There are not many hydration benchmarks as well.

krausest.github.io/js-framework-be...

indepth.dev/posts/1324/the-journey...

github.com/marko-js/isomorphic-ui-...