Hydration

We often talk about the concept of hydration and how the speed of it is vital to delivering a good experience, I want to touch on some efforts we as the Preact team have done to optimize for this metric as well as how other tools are aiming to shift us to a better place.

Say what?

For people unfamiliar with the concept, hydration is the practice of making a HTML page interactive. When we execute the following code-snippet

import { render } from 'preact'

render(
  <button
    onClick={() => {
      console.log('Oh hi, nice to have you here')
    }}
  >
    Hello world
  </button>,
  document.body
)

We will replace anything inside document.body with a button that has a click-listener attached. When we server-side render we will be receiving the following html.

<body>
  <button>Hello world</button>
</body>

Which in and of itself does nothing, we can click the button however much we like it won't do anything, we could call render again like the previous code-snipper but that would destroy and recreate the DOM. Instead we will call hydrate.

import { hydrate } from 'preact'

hydrate(
  <button
    onClick={() => {
      console.log('Oh hi, nice to have you here')
    }}
  >
    Hello world
  </button>,
  document.body
)

Now Preact will go through the existing elements in document.body and match them up against what the Virtual-DOM is saying has to be there. After this is executed our application will be interactive.

It is valuable to be aware of how this will occur in practice because we first get our HTML and our HTML file will contain a script tag telling the client where to find the code that will make this page interactive. This means we are essentially dealing with a waterfall

Download HTML --> Parse HTML --> Download JS --> Parse JS --> Execute hydration --> Page interactive

This means the bigger your JS/... the longer it will take before the user can click something or navigate further, this is called the Time To Interactive metric in the core-web-vitals metrics.

Interactivity please

We want to optimize this time-to-interactive metric as it determines the usability of your application, imagine someone downloading the HTML for your application and due to a large bundle size or a bad connection the JS loads in slow, now the users will be hitting buttons but nothing will actually be happening... This is the valley between first paint and interactiveness.

From this we can conclude there's a few things we can do to make things interactive faster

  • we leverage the platform <a> tags for our links, these work out of the box without JS, the SPA transitions however might not work
  • we start inlining our event-handlers for critical paths in the application
  • we reduce the code downloaded for the minimal interactivity
  • we reduce the amount of surface hydration has to touch before being fully interactive

The two first are interesting paths and I highly suggest looking into how Remix is trying to shift the status quo there, however for this post we will focus where front-end libraries like Preact can come in to help out.

A pretty native tool to reduce bundle-size for entry points have been async routes, as that is the first factor that will be variable in the entry of an application, we split up in multiple routes which essentially all are unused code apart from the one route we render. This leads me to the first usable heuristic in optimizing hydration, we introduce a helper to orchestrate these async loading states, currently we have these in the shape of lazy and Suspense as well as in Preact-ISO (which we should really generalize one day).

For the sake of familiarity let's look at the Suspense approach, when a route isn't loaded lazy will throw a Promise which follows a similar pattern to error boundaries in that when we see Suspense we can stop there, render the fallback and go on with our lives. What if instead of just rendering the fallback we look at whether there is DOM and mark this Suspense root as a pending hydration? At that point we have a deferred hydration going on that progressively hydrates the whole application as components are coming in.

When we apply that to the deferred routes we see that our application becomes interactive in the following order:

<body>
  <header><!-- some links --></header> <!-- Interactive in the first pass -->
  <main>
    <!--
      route content
      Interactive in the second pass
      when the route specific code arrives
    -->
  </main>
  </footer><!-- some links --></footer> <!-- Interactive in the first pass -->
</body>

This is a little known trick that currently actually already lives in Preact, I have actually created a small demo years ago to show this. The demo goes a bit further as it also introduces the above concept for data-loading, as long as data isn't loaded it will suspend and use the existing non-hydrated DOM.

Islands of interactivity

There is another alternative in the islands approach, conceptually we can approach this as removing all the parts we don't need and only hydrating little roots in our Virtual DOM. This all can happen at build-time and mainly introduces heuristics to play with, for instance only hydrate when this island becomes visible. This works out well in Tools pioneering this approach are Astro, Fresh and Capri, these are definitley worth checking out for your next project!

Hell, I have considered rewriting this very blog in either of them just because I love seeing these tools in action and honestly I really should get some static-site generation in here for SEO... Oh and probably some styling as well as I am starting to notice that my minimalism is even starting to disturb me...

Before choosing one of these approaches I always advise people to read up on the different rendering techniques. After writing about all of this I do want to shoutout my fellow maintainers on Preact Jason and Marvin as I would have never learned this much about all of the rendering techniques, hydration and Virtual-DOM without them.

There is so much innovation coming to this space when I look at streaming rendering, islands, ... and it honestly excites me beyond belief!