Konrad Kowalski (rootsher)Principal Platform & Reliability Architect100011111110110101000101101000010000001101110001

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:

text
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:

text
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:

text
hydrate search
hydrate navigation
hydrate cart
hydrate reviews
hydrate newsletter

And give them priorities.

For example:

text
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.

text
region A
  |
  v
region B
  |
  v
region C

Selective hydration means being able to choose which region gets activated earlier, regardless of tree order.

text
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:

text
navigation
search
the main article

Further down:

text
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:

text
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:

text
component enters viewport
  |
  v
hydrate

But you can use many different signals:

text
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:

text
priority = f(
  visibility,
  user intent,
  interaction,
  available CPU
)

And that is far closer to the real performance problem than a simple:

text
hydrate everything ASAP

A curiosity

Hydration competes for the main thread exactly like any other JavaScript.

If the browser has to run:

text
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:

text
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:

text
full hydration = 200 ms CPU

We can split it into:

text
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:

text
page load
  |
  v
download dropdown JS
  |
  v
hydrate dropdown

we can do:

text
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:

text
do work later

to:

text
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:

text
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:

text
whole application

or:

text
page sections

or:

text
individual components

The smaller the boundary, the more control.

But also more:

text
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:

text
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:

text
whole page is an application
some parts activate later

we can say:

text
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.