Konrad Kowalski (rootsher)Principal Platform & Reliability Architect010110010011001101101111110000100000010111011010

Hydration: connecting HTML with a working application

date
category
Frontend
reading
4 min / 723 words

SSR solves the problem of the first render.

The server can produce HTML earlier, so the user sees content before all the application's JavaScript has started.

But HTML alone is not yet an application.

A button may look like a button, a form like a form and a menu like a menu, but if the interaction depends on JavaScript, the browser still has to reconstruct the application runtime.

We call that process hydration.

HTML is only the result of rendering

Assume the server renders a component:

jsx
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );
}

What reaches the browser can be:

html
<button>0</button>

That is enough to display something.

But that HTML carries no information about:

text
useState
the setCount function
the onClick callback
the component structure
the runtime dependencies

The browser sees a DOM element.

It does not see a component.

Hydration reconstructs the application layer

Once the JavaScript loads, the framework has to recreate the structure that produced the HTML.

Simplified:

text
server

Component Tree
  |
  v
render
  |
  v
HTML
  |
  v
network
  |
  v
browser

existing DOM
  +
JavaScript
  |
  v
recreate component tree
  |
  v
connect runtime with DOM
  |
  v
interactive application

What matters is that the framework usually does not want to create the whole DOM again.

The DOM already exists.

Hydration tries to reuse the existing elements and bind them to the client-side runtime.

The name is quite apt: static HTML gets "hydrated" with application behaviour.

So SSR does not mean less work

This is a common mental model mistake.

You can assume:

text
CSR:
browser renders

SSR:
server renders

and conclude that SSR simply moves work from the client to the server.

In a classic hydrated application the situation is closer to:

text
SSR:

server render
+
client reconstruction

The server performed a render so the user would get HTML earlier.

The browser still has to start the application.

So some of the work has been added, not moved.

That can be a great trade-off, but only when showing content earlier is worth the extra cost.

A commercial example: the store looks fast but does not react

Imagine a product detail page.

The server sends:

html
<h1>Mechanical Keyboard</h1>
<p>129 zł</p>
<button>Add to cart</button>

The content appears very quickly.

The user sees the button and clicks it right away.

The problem: the application's JavaScript is still loading, or the main thread is busy with hydration.

Visually the page looks ready.

Functionally it is not yet.

That is one of the most treacherous failure modes of SSR:

text
visible
!=
interactive

The user does not care that Lighthouse praises a fast first render if a click does nothing for a moment.

Hydration has to agree with the server

There is one more problem.

The client-side render should reproduce a structure consistent with the HTML the server generated.

Assume:

jsx
function Clock() {
  return <span>{Date.now()}</span>;
}

The server renders:

html
<span>1750000000000</span>

A few hundred milliseconds later the browser runs the same component:

html
<span>1750000000473</span>

The results differ.

A hydration mismatch appears.

Similar problems can be caused by:

text
random values
the local time zone
locale
data available only in the browser
conditions that depend on window
different state on either side

So SSR requires more than the ability to run the same component on the server and on the client.

It requires a sufficiently deterministic first render.

A curiosity

HTML cannot serialise a closure.

Assume:

js
const productId = 42;

button.onclick = () => {
  addToCart(productId);
};

The server cannot send the onclick function as part of ordinary HTML together with its lexical environment.

It can send:

html
<button>Add to cart</button>

but the information that clicking should call addToCart(42) belongs to the world of the JavaScript runtime.

That is one of the fundamental reasons why server-rendered HTML cannot simply "contain a working application".

You either reconstruct the execution context on the client, or find a way to encode enough information to resume it later.

The second option will become important with resumability.

The bigger the page, the bigger the cost

Imagine a portal with:

text
a header
a menu
an article
comments
related content
ads
a newsletter
a weather widget
a user panel

If all of it is part of one hydrated tree, the browser may have to run a large amount of JavaScript even when the user only wants to read the article.

Which leads to a question:

does everything really have to be interactive right away?

If the comments are three screens down, hydrating them during startup may not be worth it.

If the footer has no interaction at all, it may not need a client runtime.

And out of that question the next strategies grow:

text
progressive hydration
selective hydration
partial hydration
islands architecture

Hydration is a scheduling problem

You can look at hydration as a framework technique.

A more interesting model, though, is:

we have a certain amount of JavaScript work to do on a limited main thread.

That work competes with:

text
user input
layout
paint
network callbacks
application logic

So the real question is not only:

how do we make hydration fast?

but also:

which part of the application do we actually have to hydrate now?

That moves the discussion from rendering itself towards priorities, scheduling and the granularity of interactivity.

And that is where the next stage in the evolution of rendering architectures begins.