Konrad Kowalski (rootsher)Principal Platform & Reliability Architect001110111010010011100011111011111100011001001101

Modern rendering is composition

date
category
Frontend
also in
Software Architecture · System Design
reading
4 min / 723 words

We have come a long way from the simple model:

text
request
  |
  v
server
  |
  v
HTML
  |
  v
browser

Then came AJAX, SPA, CSR, the return of SSR, SSG, deferred generation, revalidation, streaming, hydration, islands, resumability and Server Components.

It is easy to turn that into an acronym zoo.

A better mental model is simpler:

modern rendering architecture is a set of independent decisions about compute, cache, network and the client runtime.

You no longer pick one mode for the application

The question used to be:

text
SSR or CSR?

Later:

text
SSR or SSG?

Today the more sensible questions are:

text
when do we render?
where do we render?
what do we cache?
what do we stream?
what do we send to the client?
what do we hydrate?

These are several different axes.

One application can use many answers at once.

Take a real marketplace

Assume a platform similar to a large store or marketplace.

We have:

text
Homepage
Search
Category
Product page
Cart
Checkout
Account
Admin

Trying to set a single:

text
renderingMode = "SSR"

for the whole application is architecturally uninteresting.

Every part has different properties.

Homepage

The homepage may contain mostly marketing content:

text
hero
categories
campaign banners
popular products
footer

A large part of it changes relatively rarely.

A good model:

text
prerender
+
revalidation
+
CDN

There is no reason to render that from scratch for every user.

But a small fragment:

text
Hello, Anna
Your cart: 3

is personalised.

It should not destroy the cacheability of the whole document.

That is a place for a dynamic fragment or client-side activation.

Search is different.

A query:

text
/search?q=mechanical+keyboard

has a potentially enormous number of variants.

We are not going to generate them during the build.

So search can use:

text
request-time data
+
server render
+
client-side interactions

The first result can be produced on the server, while later:

text
filters
sorting
pagination

can run on the client.

That is neither an "SSR application" nor a "CSR application".

It is a composition of both models.

The product detail page

Here it gets even more interesting.

text
Product Page
├── description
├── technical specs
├── price
├── availability
├── recommendations
├── reviews
├── wishlist
└── add to cart

Every fragment has different properties.

We can have:

text
description
-> prerender + long cache

technical specs
-> prerender

price
-> short revalidation / dynamic

availability
-> dynamic

recommendations
-> streamed

wishlist
-> client

add to cart
-> client

On top of that some components can be server-only and never reach the browser.

So a single page uses several generations of rendering architecture at once.

Cart and checkout

The cart is yet another class of problem.

Here we have:

text
strong personalisation
a lot of interaction
optimistic updates
frequent mutations
very fresh data

There is little point in treating the whole cart UI as static content.

A client-side runtime has a lot of value here.

Checkout can still get its initial HTML from the server, though.

So:

text
server initial render
+
long-lived client state

is still a sensible model.

A curiosity

The most important boundaries of a modern application often do not match routes.

We can have:

text
/products/42

and inside it several different boundaries:

text
cache boundary
streaming boundary
server/client boundary
hydration boundary
error boundary

That is five different boundaries on the same page.

A framework may express all of them through a similar component structure, but from the runtime's point of view they solve different problems.

That is why the component tree increasingly doubles as a description of the execution architecture.

Four resources

Most decisions come down to four costs.

Compute

Where do we perform the work?

text
build machine
origin
edge
browser

Network

What has to cross the network?

text
HTML
JSON
JavaScript
component payload
images

Storage / cache

Which result can we compute once and reuse later?

text
static artifact
CDN cache
fragment cache
data cache

Client CPU

How much work do we leave to the user's device?

text
parse JS
execute JS
hydrate
render
handle interactions

New rendering techniques usually do not eliminate cost.

They move it between those four places.

SSR is not free

We move compute:

text
browser -> server

but we increase:

text
server CPU
request latency
infrastructure complexity

SSG is not free

We move compute:

text
request-time -> build-time

but we increase:

text
build cost
storage
invalidation complexity

Hydration is not free

We buy:

text
early HTML
+
client application

at the cost of:

text
server render
+
client reconstruction

Islands are not free

We reduce:

text
global client runtime

but we increase:

text
number of boundaries
state coordination
runtime fragmentation

Every architecture is an exchange of costs.

The framework should be a consequence of the decisions

That inverts a popular way of designing.

Not:

text
we use framework X
  |
  v
so we will use feature Y

but:

text
this fragment:
- changes rarely
- gets a lot of traffic
- is not personalised
  |
  v
should be prerendered and cached

And only then do we ask:

how do we implement that in our stack?

Next.js, Nuxt, Astro, SvelteKit or other meta-frameworks are tools for implementing these decisions.

They should not be their source.

The most important question: where to put the boundaries

A senior frontend engineer increasingly rarely optimises an individual:

text
render()

and increasingly often designs boundaries:

text
static | dynamic

server | client

cached | fresh

eager | lazy

blocking | streamed

A well-placed boundary can remove whole classes of cost.

A badly placed one can mean that:

text
personalisation disables the cache for the whole page
one client component pulls a large dependency graph into the browser
one slow fetch blocks the entire SSR
one large hydration boundary occupies the main thread

We did not go back to the server-rendered web

At first glance history has come full circle.

We started with:

text
server -> HTML

and we are talking about the server a lot again.

But the modern model is different.

Today we can decide separately:

text
this fragment is produced during the build
this one during the request
this one at the edge
this one we stream
this one stays server-only
this one we activate in the browser
this one we start only after a click

So we did not go back to the old SSR.

We arrived at an architecture in which rendering stopped being a single stage.

It became a problem of placing work across time, space and runtimes.

And that is probably the best way to look at the modern frontend:

not as picking the next shortcut,

but as designing the flow of compute, data and interactivity through the whole system.