Selective and progressive hydration: not everything deserves CPU now
- date
- category
- Frontend
- reading
- 4 min / 710 words
Hydration solves the problem of connecting server-rendered HTML with a working client-side application.
The trouble is that classic hydration often treats the whole tree as one large startup task.
If a page has dozens of interactive regions, the browser may do a lot of work before the user even tries to use most of them.
The natural next step is:
do not hydrate everything at once.
Hydration as a work queue
Assume a page:
header
hero
search
product filters
recommendations
reviews
newsletter
footer
Not all of these have the same priority.
Search may be needed immediately.
Reviews are three viewports down.
The newsletter may never be used at all.
Classic hydration can still treat them the same way:
load JS
|
v
recreate whole tree
|
v
attach behavior everywhere
Selective hydration tries to change that model.
What matters first
Instead of one large task we can think of many smaller ones:
hydrate search
hydrate navigation
hydrate cart
hydrate reviews
hydrate newsletter
And give them priorities.
For example:
visible + interactive
|
v
high priority
below the fold
|
v
low priority
never interacted with
|
v
maybe never needed
That turns hydration from a binary operation into a scheduling problem.
Selective and progressive are not always the same thing
Terminology differs between frameworks, but a useful split is this:
Progressive hydration means activating parts of the UI gradually.
region A
|
v
region B
|
v
region C
Selective hydration means being able to choose which region gets activated earlier, regardless of tree order.
region A waiting
region B clicked by user
|
v
hydrate B first
That is a subtle but important difference.
In the first model we do the work in batches.
In the second the scheduler can react to the user's real intent.
A commercial example: a content portal
Imagine the homepage of a large portal.
On the first screen there are:
navigation
search
the main article
Further down:
comments
sports scores
recommendations
newsletter
an interactive widget
If the user only reads the article, hydrating all of those immediately is pure cost.
A better strategy may look like this:
navigation -> immediately
search -> immediately
comments -> when visible
widget -> when visible
newsletter -> on interaction
A framework can implement that automatically or let the developer define the boundaries.
Visibility is only one signal
The simplest trigger:
component enters viewport
|
v
hydrate
But you can use many different signals:
the element becomes visible
the user hovers over it
the element receives focus
the user starts interacting
the browser has idle time
a more important task has finished
That gives a model:
priority = f(
visibility,
user intent,
interaction,
available CPU
)
And that is far closer to the real performance problem than a simple:
hydrate everything ASAP
A curiosity
Hydration competes for the main thread exactly like any other JavaScript.
If the browser has to run:
120 ms hydration task
and the user clicks after 20 ms, the click handler cannot simply "interrupt" the running JavaScript.
JavaScript on the main thread is cooperative.
Until the current task yields:
hydration hydration hydration hydration
|
v
task ends
|
v
click handler
the input waits.
That is why splitting hydration into smaller units can improve responsiveness even when the total amount of work stays identical.
Less blocking does not mean less work
This is an important distinction.
Assume:
full hydration = 200 ms CPU
We can split it into:
20 ms
20 ms
20 ms
...
The UI can then react between tasks.
But we still did 200 ms of work.
Progressive hydration mainly improves:
when the work blocks the user.
It does not have to reduce the total cost.
To really do less work, you have to go one step further.
Hydrate on interaction
Assume a dropdown most users will never open.
Instead of:
page load
|
v
download dropdown JS
|
v
hydrate dropdown
we can do:
page load
|
v
static HTML
user interacts
|
v
load required JS
|
v
activate component
Here the benefit is larger.
If the user never clicks, the cost may never be paid.
That is an important shift from:
do work later
to:
maybe never do the work
But the interaction has to be captured somehow
An interesting problem appears here.
If the component is not active yet, who handles the first click?
We need a lightweight layer that can:
capture interaction
|
v
identify required component
|
v
load code
|
v
activate component
|
v
replay / continue interaction
So delaying hydration does not remove the infrastructure.
It moves part of the responsibility into a runtime responsible for lazy activation.
Granularity matters
We can hydrate:
whole application
or:
page sections
or:
individual components
The smaller the boundary, the more control.
But also more:
code loading points
dependencies
scheduling decisions
runtime bookkeeping
There is no point in splitting an application into thousands of boundaries just because we technically can.
The boundaries should match real interactivity.
Typical solutions
Modern frameworks implement these ideas in different ways.
React develops selective hydration mechanisms combined with concurrent rendering and Suspense.
Astro lets you declare how client components should be activated with strategies like:
load immediately
load when visible
load when browser is idle
Which library uses which syntax is not the point.
The mechanism stays the same:
client CPU becomes a resource that has to be allocated by priority.
And what if most of the page never needs a runtime?
Selective hydration still assumes we have a server-rendered application tree and we choose when to activate its fragments.
But the assumption can be inverted.
Instead of:
whole page is an application
some parts activate later
we can say:
whole page is static HTML
only some parts are applications
That sounds similar, but it leads to a different architecture.
The unit of the application stops being the page.
It becomes small, independent islands of interactivity.
And that is what islands architecture grows out of.