Konrad Kowalski (rootsher)Principal Platform & Reliability Architect110000000000010111101000010000011111101100110100

SSR returns: the server renders HTML again

date
category
Frontend
reading
4 min / 719 words

CSR gave the frontend full control over the application, but the first load started to have a longer and longer critical path.

The browser had to download JavaScript, start the runtime, discover data dependencies and only then produce the UI.

The natural answer was simple:

render the first view earlier, on the server.

SSR comes back, but in a different form

The classic server-rendered web worked like this:

text
request
  |
  v
server
  |
  v
HTML
  |
  v
browser

Modern SSR adds one more stage:

text
request
  |
  v
server render
  |
  v
HTML
  |
  v
browser shows content
  |
  v
JavaScript loads
  |
  v
application takes over

That is an important difference.

The server does not render the page so that the frontend stops existing.

It renders it so that the first view exists earlier.

Afterwards the browser can still behave like an SPA.

Why do the same work on the server

Assume the user opens:

text
/products/42

In pure CSR it can go like this:

text
HTML shell
  |
  v
JS
  |
  v
router
  |
  v
fetch product
  |
  v
render

In SSR:

text
request /products/42
  |
  v
server fetches product
  |
  v
server renders HTML
  |
  v
browser receives content

The user can see the product name, price and description before the whole application runtime has started.

That shortens the path to the first content.

But it does not mean the client cost disappears.

SSR does not remove JavaScript

This is one of the most important points.

If the application is to be interactive later, the browser still often has to download:

text
the framework
components
routing
event handling
application state
the code responsible for further interactions

So SSR is not:

text
server instead of browser

It is rather:

text
server first
browser later

In practice some of the work may be done twice.

The server produces HTML, and the client later reconstructs the component runtime needed for the application to keep working.

That process will become a separate problem we will later call hydration.

TTFB starts to matter again

In CSR the server can respond almost immediately:

html
<div id="app"></div>

That gives a great TTFB, but says little about when the actual content shows up.

SSR moves work in front of the response.

text
request
  |
  v
database
  |
  v
business logic
  |
  v
render
  |
  v
response

That can increase Time to First Byte.

And here the first real trade-off appears:

text
CSR
-> fast empty response
-> more work later

SSR
-> more work before the response
-> more finished content earlier

That is why the statement:

SSR is faster

is too simple.

Faster at which point of the path?

A commercial example: the product detail page

An online store is a good case for SSR.

The user lands on a product page straight from a search engine.

The most important data is already known during the request:

text
name
price
photo
availability
description

The server can fetch it and immediately produce:

html
<h1>Mechanical Keyboard</h1>
<p>129 zł</p>
<p>In stock</p>

The browser does not have to wait for the framework to fetch and perform its first render.

At the same time an interactive cart, product variants or recommendations can start up later.

Modern frameworks such as Next.js, Nuxt or SvelteKit use this model as one of the available strategies.

A curiosity

SSR can become a CPU problem even when a single render is fast.

Assume:

text
render = 20 ms CPU

With one user that is practically nothing.

With thousands of requests per second the situation looks different.

If every request requires rendering:

text
request
request
request
request
request
  |
  v
limited CPU cores

a queue starts to form.

The user's latency is then not just:

text
render time

but:

text
queue time + render time

That is why under heavy traffic optimising SSR becomes a problem of concurrency, cache and capacity planning, not only of the framework.

Personalisation complicates the cache

SSR fits dynamic data very well.

But the more the response depends on the request, the harder it is to cache.

An example:

text
/products/42

If every user sees the same HTML, the response can easily live on a CDN.

But if the page depends on:

text
user
country
currency
experiment
session
pricing tier

the number of possible variants grows.

In practice you have to decide what really needs to be dynamic.

Often only a small fragment of the page is personalised, while the rest could be shared by everyone.

That question will later lead to more granular rendering models.

SSR can create a waterfall too

Moving the render to the server does not remove data dependencies.

It can only move them somewhere else.

Badly written SSR:

text
fetch user
  |
  v
fetch product
  |
  v
fetch reviews
  |
  v
fetch recommendations
  |
  v
render

means the user waits for the whole sequential chain before receiving the first byte of HTML.

So the server can create exactly the same waterfall we used to have in the browser.

Except now the waterfall blocks the entire response.

SSR solves one problem and creates the next

Moving rendering back to the server improved the first display of content, but a question appeared:

what next?

We have finished HTML.

But the browser still needs JavaScript for the buttons to work, for state to be current and for navigation to stay client-side.

So we have to connect:

text
HTML generated on server

with:

text
application runtime in browser

That connection is neither free nor trivial.

Which is exactly why the next big element of modern rendering architecture became hydration.