Konrad Kowalski (rootsher)Principal Platform & Reliability Architect100100010010101100010001100010010000000110100100

Partial Prerendering: one page, several clocks

date
category
Frontend
also in
Caching
reading
3 min / 633 words

For a long time we assigned a rendering strategy to a whole route.

A page was:

text
static
rendered on the server
or rendered on the client

The trouble is that real pages are very rarely homogeneous.

A product detail page can have a static description, a dynamic price, a session-dependent cart and recommendations that arrive later.

So why should the whole page share one lifecycle?

A route is too large a unit of decision

Assume:

text
/products/42

On that page we have:

text
header              -> almost always the same
product description -> changes rarely
price               -> changes often
cart                -> depends on the user
recommendations     -> slow backend
footer              -> practically static

If we treat the route as one unit, we face a poor choice.

Everything as SSR:

text
request
  |
  v
render everything
  |
  v
send

means the static fragments are computed on every request.

Everything as SSG:

text
build
  |
  v
render everything

means a problem with the price and the cart.

The natural answer is:

render different fragments at different times.

A static shell with dynamic holes

The mental model of Partial Prerendering can be reduced to:

text
┌──────────────────────────────┐
│ STATIC                       │
│ header                       │
│ product description          │
│                              │
│   ┌──────────────────────┐   │
│   │ DYNAMIC PRICE        │   │
│   └──────────────────────┘   │
│                              │
│   ┌──────────────────────┐   │
│   │ DYNAMIC CART         │   │
│   └──────────────────────┘   │
│                              │
│ footer                       │
└──────────────────────────────┘

The static part can be produced earlier and cached aggressively.

The dynamic fragments are filled in later, for example during the request or through a stream.

This is no longer:

text
static page

nor:

text
dynamic page

It is a composition of both models.

A commercial example: e-commerce

A store is the ideal case here.

The product description, photos or technical specification may change rarely.

The price may depend on a promotion.

The cart depends on the user's session.

Availability can change very often.

Recommendations may come from a slow ML system.

Assigning all of those one TTL or one moment of rendering is artificial.

A better model:

text
description -> prerender
price       -> request-time
cart        -> user-specific
recs        -> streamed later

Modern meta-frameworks increasingly let you model a page exactly that way.

The cache boundary matters more than the route boundary

This leads to an interesting shift in the mental model.

In classic SSG we asked:

can this page be static?

Now the question is:

which part of this page can be static?

That is far more precise.

We can have a fragment that is safe to share with a million users next to a fragment that exists for a single session.

text
shared cache
  |
  v
static shell

request-specific compute
  |
  v
personalized fragment

Thanks to that, personalisation does not have to destroy the cacheability of the whole document.

A curiosity

You can treat Partial Prerendering like rendering with placeholders.

Imagine the build producing:

html
<header>...</header>

<section class="product">
  ...
</section>

<!-- PRICE_SLOT -->

<!-- CART_SLOT -->

<footer>...</footer>

and a request-time runtime later filling in the missing fragments.

Real implementations are of course more complex, but the mental model is useful:

text
precomputed structure
+
late-bound values

Similar ideas have existed for a long time in templating systems, edge-side includes or fragment caching.

So the novelty is not the idea of "holes" itself, but how much better this model now integrates with components, streaming and the client runtime.

The boundaries have to be chosen well

Partial rendering sounds great, but granularity has a cost.

If every small piece of UI becomes a separate dynamic boundary:

text
fragment
fragment
fragment
fragment
fragment
fragment

the system has to manage more:

text
dependencies
fetches
cache entries
streamed fragments
fallbacks
failure boundaries

Too much granularity can create more overhead than savings.

That is why good boundaries usually match real differences in:

text
freshness
personalization
latency
cacheability

Not simply the component structure.

A component is not always a good rendering boundary

This matters for frontend engineers.

We may have a component:

text
<ProductCard />

but inside:

text
name         -> static
image        -> static
price        -> dynamic
availability -> dynamic
favorite     -> user-specific

A component boundary is organisational.

A cache or rendering boundary is architectural.

Those are two different things.

A framework may merge them for convenience, but a senior frontend engineer should understand they are not identical.

Partial Prerendering pairs well with streaming

The static shell can be available immediately.

The dynamic fragments can be produced in parallel:

text
static shell
  |
  v
SEND

price ------------------> SEND
cart -------------------> SEND
recommendations --------------------> SEND

That gives a very interesting model:

text
part of the output existed before the request
part is produced during the request
part arrives later through the stream

So one page has several different "clocks".

But where should the dynamic fragments run?

Since part of the rendering happens on the request path, the next question appears:

text
user in Warsaw
  |
  v
where should compute happen?

At an origin in the US?

In a European region?

On an edge node close to the user?

That is another independent axis.

Partial Prerendering answers:

which part of the page is produced when?

It does not answer:

where do we physically perform that work?

And that is exactly where we get to edge rendering.