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:
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
What reaches the browser can be:
<button>0</button>
That is enough to display something.
But that HTML carries no information about:
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:
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:
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:
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:
<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:
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:
function Clock() {
return <span>{Date.now()}</span>;
}
The server renders:
<span>1750000000000</span>
A few hundred milliseconds later the browser runs the same component:
<span>1750000000473</span>
The results differ.
A hydration mismatch appears.
Similar problems can be caused by:
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:
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:
<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:
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:
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:
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.