Edge rendering: compute moved closer to the user
- date
- category
- Frontend
- also in
- Cloud · System Design
- reading
- 4 min / 765 words
So far we have been moving rendering mostly in time.
It could happen:
during the build
on the first request
on every request
or in fragments through streaming
But there is a second axis:
where is the code physically executed?
If the user is in Warsaw, the application runs in us-east-1 and the database sits in Virginia, every dynamic request carries a geographic cost.
The natural idea is:
move the compute closer to the user.
The origin does not have to be the only place of execution
The classic model:
browser
|
v
CDN
|
v
origin
|
v
database
A CDN handles static assets beautifully.
But dynamic HTML still often requires a round trip to the origin.
Edge rendering tries to move part of the execution:
browser
|
v
edge runtime
|
v
response
If the user is a few milliseconds from an edge node, the first part of the request can be much shorter.
"Closer to the user" sounds better than it is
Assume:
user -> edge 10 ms
user -> origin 120 ms
At first glance the edge wins hands down.
But a render usually needs data.
If the database still sits next to the origin:
user
| 10 ms
v
edge
| 110 ms
v
database
| 110 ms
v
edge
| 10 ms
v
user
then we moved compute closer to the user but further from the data.
In practice the most important question is not:
where is the renderer?
but:
where is the whole dependency graph needed for the response?
A commercial example: a global storefront
Imagine a store operating in Europe, the US and Asia.
Part of the page is shared:
layout
product description
photos
marketing content
Part depends on the region:
currency
taxes
promotions
availability
language
An edge runtime can be a very good fit for lightweight logic:
request
|
v
detect region
|
v
select locale
|
v
read edge cache
|
v
compose response
If the needed data is already available locally, you can avoid an expensive round trip to a central origin.
That is why the edge combines well with:
CDN cache
KV storage
geolocation
routing
simple personalization
An edge runtime is often a different runtime
In practice "edge" does not always mean simply:
Node.js, only closer to the user
The code often runs in a more constrained environment.
For example, in an isolate-based model:
request
|
v
existing lightweight isolate
|
v
execute handler
instead of:
request
|
v
start process/container
|
v
initialize runtime
|
v
execute handler
That can reduce startup overhead, but usually limits the available APIs and the way native dependencies can be used.
A framework may hide much of that difference, but the runtime is still a different one.
A curiosity
A V8 isolate is not a separate process.
It is an isolated JavaScript execution context inside a V8 process, with its own heap and runtime state.
Thanks to that many isolates can share one process instead of starting a separate system process for every request.
That gives a very good startup and density profile.
But an isolate boundary does not automatically provide the same isolation as a process boundary.
That is one of the reasons edge runtimes come with their own set of constraints and sandboxing mechanisms.
Data locality beats marketing
Assume two variants.
Origin rendering
user -> origin: 100 ms
origin -> database: 2 ms
render: 15 ms
Edge rendering
user -> edge: 10 ms
edge -> database: 90 ms
render: 15 ms
The edge gives no spectacular advantage here.
If the render runs several sequential queries:
edge
|
v
DB query A
|
v
DB query B
|
v
DB query C
the problem may be even worse.
Every database connection adds another RTT.
That is why edge architecture very often forces a change not only in where the frontend runs but also in how it accesses data.
Data replication changes the game
If we can replicate the data:
US database
EU replica
Asia replica
then compute and data can sit close to each other.
But a new problem appears immediately:
consistency.
If a user changes their delivery address in Europe, when will the update be visible in the Tokyo replica?
For marketing content a delay of a few seconds may be irrelevant.
For account state or payments it is not.
So the edge pushes the frontend into the world of classic distributed systems problems:
latency
consistency
replication
data ownership
The edge is great for a certain class of work
Not everything has to move.
Good candidates are usually operations that are:
short
request-specific
location-dependent
highly cacheable
free of many distant queries
For example:
redirect
locale detection
A/B assignment
authentication check
cache lookup
response composition
A much harder case:
complex ORM query
+
multiple internal APIs
+
transaction
+
large server render
Deploying "to the edge" does not by itself fix a dependency architecture.
Edge and SSR are different decisions
This is an important distinction.
SSR answers:
when is the HTML produced?
Edge answers:
where is the code executed?
We can have:
SSR on origin
SSR on edge
static HTML on edge CDN
client rendering with edge API
Those are independent axes.
Modern frameworks often present them together, which makes it easy to start treating "edge rendering" as a separate type of rendering.
Architecturally it is above all a decision about compute location.
And again we come back to the client
So far we have been trying to improve the first delivery of HTML.
We moved rendering:
build
request
stream
edge
But the second half of the problem remains.
If we ship the user a large JavaScript application, the browser may still perform a full hydration no matter how fast the HTML was produced.
So we can optimise the server to its limits and then hand the main thread an enormous component tree to activate.
The natural question is:
does the whole page really have to become interactive right away?
That leads to the next class of solutions: selective and progressive hydration.