Partial Prerendering: one page, several clocks
For a long time we assigned a rendering strategy to a whole route.
A page was:
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:
/products/42
On that page we have:
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:
request
|
v
render everything
|
v
send
means the static fragments are computed on every request.
Everything as SSG:
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:
┌──────────────────────────────┐
│ 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:
static page
nor:
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:
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.
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:
<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:
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:
fragment
fragment
fragment
fragment
fragment
fragment
the system has to manage more:
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:
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:
<ProductCard />
but inside:
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:
static shell
|
v
SEND
price ------------------> SEND
cart -------------------> SEND
recommendations --------------------> SEND
That gives a very interesting model:
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:
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.