SPA and CSR: when the browser took over the application
- date
- category
- Frontend
- reading
- 4 min / 899 words
AJAX solved one specific problem: a request no longer had to mean a full navigation.
The next step was logical.
If the browser can fetch data in the background and update only fragments of the UI, why should the server keep rendering a new document for every transition between screens?
That is how the era of Single Page Applications began.
One document, many views
In the classic model:
/products
|
v
server render
|
v
HTML document
/products/42
|
v
server render
|
v
another HTML document
In an SPA:
initial document
|
v
JavaScript application
|
v
client-side router
|
v
fetch data
|
v
render next view
The browser downloads the main document once and then manages the transitions between screens itself.
The URL can still change, but it no longer has to mean downloading a new document.
That changes the role of the frontend fundamentally.
The frontend stops being a document presentation layer.
It becomes a long-lived application runtime.
CSR is not the same thing as SPA
These terms often get thrown into one bag, but they describe different things.
SPA is mostly about the navigation model:
the application runs inside a single document.
CSR is about where rendering happens:
the UI is produced on the client.
The typical model looks like this:
GET /
|
v
minimal HTML
|
v
download JavaScript
|
v
execute application
|
v
fetch data
|
v
render UI
The server no longer has to generate a finished product view.
It can return a shell:
<div id="app"></div>
<script src="/app.js"></script>
And the rest happens on the client.
Why this was attractive
For applications like mail, a CRM, a dashboard or a document editor, the document model starts to get uncomfortable.
Imagine an analytics system.
The user:
changes the date range
opens a modal
filters a table
switches a tab
expands a chart
selects records
refreshes data in the background
A full navigation after each of those operations would be absurd.
An SPA lets you keep:
application state
component state
cached data
open connections
current UI state
for the whole session.
That is exactly why CSR still makes a lot of sense in many applications behind a login.
For an internal admin panel, the time to first HTML may matter less than the smoothness of the next few hundred interactions.
The server changes role
In this model the backend increasingly stops returning views.
It returns data:
GET /api/products/42
|
v
{
"id": 42,
"name": "Mechanical Keyboard",
"price": 129
}
And the browser does:
data
|
v
application state
|
v
render
|
v
DOM
This leads to a very important separation:
backend
-> data + business logic
frontend
-> application state + rendering
In practice the line is not always that clean, but this is when the API + frontend application model starts to settle in.
The problem of syncing the DOM by hand
In a larger application a hard problem shows up very quickly.
You have state:
{
user,
cart,
filters,
products,
selectedProduct,
notifications
}
And a DOM that is supposed to be its current representation.
If you update the DOM manually, you have to watch every dependency.
state changes
|
v
which elements are affected?
|
v
which listeners survive?
|
v
which derived values change?
|
v
which DOM nodes need replacement?
This is where frontend frameworks start to grow.
Not because document.querySelector() is too hard.
The problem is keeping consistency between changing state and a large UI tree.
React will later describe the idea very elegantly:
UI = f(state)
Instead of telling the browser exactly which elements to change, you describe what the UI should look like for a given state.
The framework takes the synchronisation on itself.
A curiosity
A client-side router does not "navigate" in the same sense the browser does.
When you run:
history.pushState({}, "", "/products/42");
the browser changes the URL and the history, but does not automatically fetch a new document.
It is up to the application to interpret the new URL:
URL changed
|
v
router matches route
|
v
load code/data
|
v
render view
That looks like a small detail, but it means a huge shift in responsibility.
In classic navigation the browser owns the protocol of the whole process.
In an SPA a large part of that lifecycle becomes application code.
The cost: the browser gets more and more work
CSR gives a great interaction model, but it moves a significant part of the cost onto the user's device.
A typical first-render path can look like this:
HTML
|
v
discover JS
|
v
download JS
|
v
parse
|
v
compile
|
v
execute
|
v
initialize framework
|
v
render
|
v
discover required data
|
v
request data
|
v
render again
And this is where a problem appears that is easy to miss at first.
The network may deliver the document very quickly, and the user is still looking at an empty shell.
Download is only part of the JavaScript cost
Frontend developers spent a long time looking mainly at bundle size.
For example:
app.js - 400 KB
But 400 KB of JavaScript costs more than the download alone.
The browser has to go through further stages:
download
|
v
decompress
|
v
parse
|
v
compile
|
v
execute
On a powerful laptop this may be almost invisible.
On a weaker phone the difference becomes significant.
On top of that, JavaScript competes for the main thread with:
input
layout
paint
other tasks
That is why two pages with identical transfer size can have completely different startup performance.
CSR can also create a waterfall
Assume the HTML contains only:
<div id="app"></div>
<script src="/app.js"></script>
The browser does not yet know that the application needs /api/products.
Finding that out looks like this:
HTML
|
v
app.js
|
v
execute
|
v
discover data dependency
|
v
/api/products
Every arrow can mean more latency.
The later we discover a dependency, the later we can start fetching it.
This will later be one of the reasons for moving part of the rendering back to the server.
Commercially CSR still makes a lot of sense
Imagine a tool like Figma, Notion or a rich trading terminal.
Once inside the application, a user may work for an hour.
In such a system the initial startup cost is acceptable if what you get afterwards is:
instant transitions between views
local state
optimistic updates
background synchronization
heavy interactivity without navigation
So CSR is not "the old model that SSR replaced".
It is still a sensible architecture for a specific class of applications.
The problem shows up when we start applying it everywhere.
A landing page, an article, a product listing and an admin panel do not share the same properties.
And around the mid-2010s the web started to feel the cost of the approach more and more:
ship everything
-> execute everything
-> render everything
-> then show content
That is when the pendulum started swinging back towards the server.
Not to kill the SPA.
To recover one thing the classic web had from the very beginning:
HTML available before the JavaScript application has a chance to start.