Server-rendered web: before the frontend became an application
- date
- category
- Frontend
- reading
- 4 min / 729 words
The early web was architecturally simpler, but not necessarily primitive.
The browser sent a request. The server ran code, fetched data, assembled HTML and sent back a complete document. Clicking a link started the whole cycle again.
browser
|
| GET /products/42
v
server
|
| query database
| execute application logic
| render HTML
|
v
browser
|
v
parse + layout + paint
Today we would call this Server-Side Rendering, but back then there was no need to add SSR to the name. It was simply the web.
The server owned the view state
A typical application from the early 2000s worked on a very simple model:
HTML = render(request, session, database)
The request carried the URL, form parameters and cookies. From those the server decided which document the user should see.
The technologies varied:
PHP
ASP / ASP.NET
JSP
Ruby on Rails
Django
later all kinds of template engines
The mechanism stayed the same.
$product = findProduct($_GET["id"]);
render("product.php", [
"product" => $product
]);
The browser did not have to know where the product price came from, whether the user was logged in, or how to bind data to a component.
It received a result:
<h1>Mechanical Keyboard</h1>
<p>$129</p>
<button>Add to cart</button>
The line of responsibility was very clear.
The server owned the application state, the browser owned mostly the document state.
Every navigation was a document transaction
Clicking:
<a href="/products?page=2">Next</a>
did not mean changing local router state.
It meant:
current document
|
v
destroy
|
v
HTTP request
|
v
server render
|
v
new document
That has an interesting consequence: many of the problems we fight on the frontend today barely existed.
There was no need to think about:
synchronising a client cache with the backend
cleaning up a component that has been alive for eight hours
hydration mismatch
bundle splitting
client-side routing
stale closures
keeping a huge application state in browser memory
A new document meant, in practice, resetting a large part of the environment.
The price was obvious: every meaningful interaction required a round trip to the server and usually a document reload.
It works surprisingly well for many products
Imagine an online store.
The user opens:
/products/mechanical-keyboard
During a single request the server can:
1. fetch the product
2. check the price
3. check the promotion
4. read the session
5. generate recommendations
6. render HTML
For content-heavy applications this is still a very reasonable model.
HTML shows up without having to download an application bundle first. Links are real links. The browser navigates according to its own native document model.
Modern systems still use exactly this idea. Next.js, Nuxt, SvelteKit or plain Rails can all generate a document on the server for a request.
What changed is mostly the scale and the way things are composed.
Where the problem shows up
Now assume the user changes the quantity of a product in the cart.
In the classic architecture that can look like this:
click "+"
|
v
POST /cart
|
v
server
|
v
database
|
v
render entire page
|
v
HTTP response
|
v
parse entire document
|
v
paint
To turn:
Quantity: 1
into:
Quantity: 2
we go through a full navigation.
This is not a performance problem of HTML rendering itself. The problem is interaction granularity.
The server operates on documents, while the user increasingly wants to operate on fragments of the interface.
And this is exactly where the pressure starts that will later lead to AJAX, and eventually to SPA and CSR.
A curiosity
A new document does not simply mean replacing document.body.
The browser runs the whole navigation lifecycle: it fetches the response, creates a new Document, parses the HTML, discovers dependent resources, builds the structures needed for styling and layout, and then prepares new frames for display.
Some CSS, JavaScript, fonts or images may be recovered from cache, but the document boundary itself remains a very strong boundary of the browser lifecycle.
Modern SSR is not the same thing
It is easy to look at a modern React application rendered on the server and say:
we are back to what PHP did 20 years ago.
Only partly.
A classic server-rendered application often finished its work once the document was delivered.
Modern SSR often does something else:
server
|
v
HTML
browser
|
v
HTML already visible
+
JavaScript application
|
v
hydration
|
v
long-lived client runtime
So the server renders the initial state of the interface, but afterwards the browser takes over the application.
That is a fundamental difference.
The classic model was above all document-oriented.
Modern frameworks are usually application-oriented, even when the first document is produced on the server.
Why we moved away from that model
Not because rendering HTML on the server was slow.
The problem was more fundamental.
The web started to demand interactions for which a document was too large a unit of update.
Autocomplete should not reload the page.
Changing a table sort should not destroy the whole document.
A chat cannot run a full navigation after every message.
Google Maps cannot reload the page every time the map is dragged.
We needed a way for the browser to talk to the server without replacing the entire document.
Technically, many of the pieces existed earlier. It was only in the mid-2000s, though, that this model became popular and got a name that stayed with us for a long time:
AJAX.
And from that moment the line between "a web page" and "an application running in the browser" started to blur quickly.