Konrad Kowalski (rootsher)Principal Platform & Reliability Architect000110000100110000101100011100010100110110101011

Resumability: what if we don't hydrate at all?

date
category
Frontend
reading
4 min / 742 words

Hydration assumes the browser receives HTML rendered on the server and then reconstructs the client-side runtime needed for the application to keep working.

Even if we limit hydration to individual islands, we still often run a similar cycle:

text
server
  |
  v
render component
  |
  v
HTML

browser
  |
  v
load JS
  |
  v
recreate component state
  |
  v
attach behavior

Resumability tries to change the very nature of that process.

Instead of reconstructing the application on the client, we try to resume execution where the server left it.

Hydration reconstructs, resumability continues

The simplest comparison:

text
hydration:

server executes
  |
  v
HTML
  |
  v
browser executes again
  |
  v
runtime reconstructed

Resumability:

text
server executes
  |
  v
HTML + serialized execution metadata
  |
  v
browser
  |
  v
continue when needed

This does not mean literally freezing the CPU on the server and shipping stack frames across the network.

It means recording enough information that the client does not have to eagerly rebuild the whole application structure.

The problem with classic hydration

Assume a page with 200 interactive components.

During the entire session the user may click three of them.

Classic hydration can still do the work needed to activate all 200.

text
200 components
  |
  v
download code
  |
  v
initialize runtime
  |
  v
reconstruct state
  |
  v
attach behavior

That is classic eager execution.

We pay the cost now because we assume it may be needed later.

Resumability tries to invert the model:

text
component exists
  |
  v
no interaction
  |
  v
no execution

user clicks
  |
  v
load exact code needed
  |
  v
resume

A commercial example: a large e-commerce portal

Imagine a product page containing:

text
a gallery
a wishlist
a variant selector
tooltips
recommendations
an FAQ accordion
reviews
a delivery calculator
a newsletter
recently viewed

Most users will not use all of those features.

If we activate the whole runtime during page load, the client pays for potential interactivity.

A resumable model can try to move the cost closer to the moment of real use.

text
page load
  |
  v
mostly HTML

user opens delivery calculator
  |
  v
load calculator logic
  |
  v
continue interaction

That is a more aggressive form of lazy execution than plain lazy loading of a component.

HTML alone is not enough

Here we return to a fundamental problem.

Assume:

jsx
function Product({ id }) {
  const add = () => addToCart(id);

  return <button onClick={add}>Add to cart</button>;
}

The server can produce:

html
<button>Add to cart</button>

But to resume the interaction the browser has to know:

text
this button
  |
  v
needs handler X
  |
  v
handler requires product id 42
  |
  v
handler implementation lives in chunk Y

That means the server-rendered output has to carry additional metadata describing the links between the DOM, the state and the code.

So resumability requires a more deliberate transfer format than HTML alone.

A curiosity

The hardest part is not serialising data.

We have been able to do that for a long time:

js
JSON.stringify({
  productId: 42,
  quantity: 1
});

The problem is serialising execution dependencies.

A closure:

js
() => addToCart(product.id)

is not a simple value.

It carries a reference to code and to a lexical environment.

So a resumable framework has to transform the program in a way that makes it possible to find, later:

text
handler
state
code location
dependencies

without having executed the whole component tree first.

That is why resumability is largely a compiler and runtime problem, not just an SSR technique.

An event can trigger loading the code

In a classic application:

text
load page
  |
  v
load component JS
  |
  v
attach click handler
  |
  v
wait for click

A resumable runtime can reverse the order:

text
load page
  |
  v
wait for click
  |
  v
identify handler
  |
  v
load handler code
  |
  v
execute

That is a very interesting shift.

JavaScript stops being loaded because a component exists.

It is loaded because execution became necessary.

Who captures the first event?

Of course something still has to run on the client.

If the user clicks:

html
<button>Add to cart</button>

the browser has to know the click matters to the application.

That is why resumability usually needs a small bootstrap runtime.

Its job can be:

text
event
  |
  v
inspect metadata
  |
  v
find required code
  |
  v
load chunk
  |
  v
restore required state
  |
  v
execute handler

So JavaScript does not disappear.

What changes is its granularity and moment of execution.

The cost moves somewhere else

Every optimisation costs something.

Resumability can reduce startup CPU and the amount of eager JavaScript, but it requires:

text
serialising additional state
metadata in the HTML
code transformation during the build
granular chunks
a runtime that resolves dependencies

So we can reduce:

text
initial execution cost

at the price of more complexity in:

text
the compiler
+
the delivery format
+
the runtime

That is a very typical architectural trade-off.

A typical solution: Qwik

The best-known modern example is Qwik.

Its architecture is designed around resumability rather than classic eager hydration.

The framework tries to serialise the information needed for later execution and to load code as granularly as possible, only when it is needed.

More important than the library itself is the idea:

starting an application does not have to mean starting the whole application.

Resumability is not automatically better

For a page with a lot of content and relatively few interactions the model is very attractive.

But imagine an application like Figma.

The user immediately:

text
selects elements
drags them
changes properties
uses shortcuts
opens panels
modifies shared state

A large part of the application becomes active very quickly.

The cost of lazy resolution may then matter less, and a classic long-lived runtime may be the simpler model.

Again, there is no single best architecture.

The next boundary: code that never reaches the client

Resumability asks:

can we avoid running client code until we really need it?

But you can go even further.

What if some components should never have a client-side implementation at all?

If a component:

text
fetches data from a database
performs a server-only transformation
renders a static part of the UI

then there may be no reason for its code to reach the browser.

At that point we stop optimising only the moment of execution.

We start splitting the module graph itself into server and client code.

And that is exactly where Server Components come in.