This article is published in English.
Browser, Server or Build Time: A Decision Map of Frontend Architectures
See how SSG, SSR, streaming, Server Components, BFFs, edge rendering, modular monoliths and micro-frontends each answer one question: where should the work happen?
Architecture discussions, and senior frontend interviews in particular, rarely care what you built. They care why you built it that way, and "it is what the team already had" is not an answer. The good news is that nearly every frontend architecture pattern responds to the same underlying question: how much work should happen in the browser, how much on the server, and how much at build time? Once you see SSR, Server Components, backends-for-frontends and micro-frontends as different ways of drawing that line, choosing between them, and defending the choice, becomes much easier.
A word on sources: the company experiences described below come from public engineering write-ups and official documentation, and they are attributed as such.
How the line between client and server moved
The early web was simple. Static HTML files loaded almost instantly and had very little that could break.
Server-side MVC frameworks such as Django, Rails and ASP.NET then started generating HTML on every request. Pages could now show dynamic data, but every click meant a full page reload.
Single-page applications built with React, Vue or Angular swung the pendulum hard the other way. The browser took over routing, state, validation and sometimes even authentication. The result was a UI that feels instant, but only after it has loaded. That qualifier is the catch: large JavaScript bundles, a slow first paint, and weaker discoverability. Google does execute JavaScript, yet rendering can delay indexing on large sites, and many other crawlers, including social media preview bots and many AI crawlers, never run scripts at all. Content they cannot see effectively does not exist for them.
Seen from a distance, the history of frontend architecture is a series of moves between thin clients, where the server does most of the work, and thick clients, where the browser does. Each pattern in the rest of this guide is another position for that boundary.
Backend-for-Frontend: reshaping one API per client
In a Backend-for-Frontend (BFF) setup, the frontend team runs a thin service of its own in front of the real backend services. That service does one job: translate backend data into exactly what a single client needs.
The pattern is usually traced back to SoundCloud. Around 2013, while splitting a monolith into microservices, the company found that web, iOS and Android clients were all competing over one shared API that suited none of them. Each client got a dedicated, lightweight backend of its own. Phil Calçado, who worked on that migration, later described its history in detail, and Sam Newman's write-up turned it into the widely cited pattern description that gave it its name. Netflix arrived at a similar solution independently, with device-specific adapter layers, because a television app and a phone app want very different payloads.
Consider a concrete case. A mobile app needs a product's name, price and thumbnail. The web app additionally needs reviews, stock levels and related items. A single shared endpoint either sends mobile far too much or sends web too little, and teams end up bolting on query parameters to compensate.
A BFF solves this by giving each surface its own tailored response. The Express service below, which relies on the fetch built into Node 18 and later, calls the upstream product API and returns only four fields, renaming title to name and reducing the image list to a single thumbnail:
// bff.js — Node 18+, fetch is built in
import express from 'express';
const app = express();
const API = 'https://api.example.com';
app.get('/products/:id', async (req, res) => {
const response = await fetch(`${API}/products/${req.params.id}`);
const data = await response.json();
res.json({
id: data.id,
name: data.title,
price: data.price,
thumbnail: data.images[0],
});
});
app.listen(4000, () => console.log('BFF listening on :4000'));
The client requests /products/123 and receives precisely the shape it wants, with no surplus fields and no extra round trips. In production code you would also check response.ok before parsing, and guard against products that have no images, since data.images[0] assumes at least one exists.
The cost deserves more attention than it usually gets. A BFF is another service to deploy, monitor and keep healthy. If it fails, the UI fails with it, even when the real backend is perfectly fine. It is not free infrastructure; it is an extra point of failure whose main payoff is a more convenient life for the frontend team. For a deeper look at building one inside a Next.js app, see turning Next.js route handlers into a deliberate BFF layer.
Rendering strategies: where the HTML is produced
Rendering has changed more than any other area in recent years, and in practice the categories blur more than diagrams suggest.
Static generation and incremental regeneration
Static Site Generation (SSG) renders every page at build time and serves plain files from a CDN. Nothing is faster or cheaper to serve, but content stays frozen until the next deployment.
Incremental Static Regeneration (ISR) adds a release valve: a page can rebuild itself in the background after a configured interval. You keep most of the speed of static files without redeploying whenever content changes.
Server-side rendering and streaming
Server-Side Rendering (SSR) produces HTML for each request. In its classic form the server sends the complete page, and then the browser hydrates it: it downloads the JavaScript bundle and attaches event handlers and state to markup that is already on screen.
React 18 introduced streaming SSR through renderToPipeableStream, which sends chunks of HTML as soon as each part of the tree is ready instead of waiting for the slowest component. Streaming is the usual choice for new applications, although many production apps still run classic all-at-once SSR without trouble.
Server Components, islands and resumability
React Server Components (RSC) and island architectures push further: only the interactive parts of a page ship JavaScript at all. Static content stays plain HTML with nothing to hydrate. Astro's islands apply this idea outside React. Qwik goes further still with resumability, which largely avoids hydration by serializing application state into the HTML so the client can pick up where the server left off.
The example below shows a Server Component page in the Next.js App Router. From Next.js 15 onward, params is a Promise that must be awaited, which is why the component destructures id only after await params. The data fetch runs on the server, so the product name and price arrive as ready-rendered HTML:
// app/products/[id]/page.tsx (Next.js 15+)
import { fetchProduct } from '@/lib/api';
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await fetchProduct(id); // runs on the server
return (
<main>
<h1>{product.name}</h1>
<p>${product.price}</p>
<AddToCartButton productId={product.id} />
</main>
);
}
Only AddToCartButton ships client-side JavaScript. For that to hold, it has to live in its own file marked with the 'use client' directive and be imported into the page; the snippet omits that import for brevity. The result is a small bundle that is interactive where interaction matters and static everywhere else.
Edge rendering and why the industry partly reversed course
Edge rendering is one of the clearest examples in modern frontend of an idea being tested in public and revised.
Between roughly 2021 and 2023 the pitch was compelling: run SSR at the edge, on platforms such as Cloudflare Workers or Vercel Edge Functions, in a data center close to each user. Shorter distance, faster pages. Vercel promoted this approach heavily.
Vercel later reversed its position openly; its then VP of Product summed it up as "this one fooled me". The reason is instructive. Compute needs to be near the user, but it also needs to be near the data, and most databases live in a single region. An edge function in Tokyo that makes several round trips to a database in Virginia is often slower than simply rendering in Virginia. When Vercel measured this on its own product, v0, plain Node.js rendering outperformed edge rendering. Vercel subsequently moved away from standalone Edge Functions and now recommends the Node.js runtime with compute placed in the same region as the data; check the current platform documentation for the exact status of each runtime.
What survived is a narrower idea: deliver the static shell of a page from the edge immediately, then stream in the dynamic parts from compute located next to the data. That is broadly what Partial Prerendering does; the partial pre-rendering and concurrent rendering explainer covers the mechanics.
Cloudflare Workers remains a genuine edge SSR platform and works well when the data itself is globally distributed. The durable lesson is that data locality usually beats user locality. Knowing why the industry changed its mind is more valuable than knowing the buzzword.
The modular frontend monolith
When a single-page app grows beyond a handful of teams, a flat repository becomes risky. Everyone edits the same shared components, and nobody is sure who owns what.
A modular monolith separates the codebase into two layers while keeping one deployable:
- A platform layer, owned by a platform team, containing the design system, shared hooks, logging and similar infrastructure.
- A domain layer of feature folders such as
user/orpayments/, each owned by a feature team.
The motivation resembles clean or hexagonal architecture, minus most of the formality. Complete clean architecture is usually overkill on the frontend; a button and a fetch call do not need three layers of abstraction between them. What matters is clear ownership and enforced boundaries, for instance lint rules that stop one domain from importing another's internals.
Micro-frontends: independence at a price
A micro-frontend architecture treats each domain as a separately deployable mini-application, typically loaded at runtime by a shell application through a mechanism such as Webpack Module Federation.
What you gain is genuine autonomy: teams release on their own schedules and can, if truly necessary, even use different frameworks. Zalando, IKEA and DAZN have all described running this at scale, always with large engineering organizations and substantial investment in shared tooling. micro-frontends.org remains the standard reference for the full case.
The failure mode that actually hurts
The problem that recurs in real incident reports is not mixing frameworks. It is shared dependency drift. One remote application upgrades a shared library while another does not, and suddenly two copies of React are running on the same page and competing for the same DOM. Module Federation can declare shared singletons and version ranges to prevent this, but only if teams agree on and enforce those constraints. That specific coordination problem is what burns teams, far more than the vague "complexity" usually cited.
There is also a cautionary example in the other direction. Spotify reportedly experimented with an iframe-based micro-frontend approach in its desktop client years ago and later consolidated into a unified architecture, partly because the seams between pieces cost more than the independence returned. Even at large scale, the pattern is not an automatic win.
Let team size drive the decision
The question that rarely appears on architecture diagrams is how many engineers you actually have. The following ranges are heuristics drawn from how teams tend to describe the decision afterwards, not hard rules:
- Fewer than about 15 engineers: a modular monolith almost always wins. There are not enough people to justify separate deployment pipelines.
- Roughly 15 to 50 engineers with a few clear domain boundaries: BFFs combined with a well-organized modular monolith usually suffice. Micro-frontends are probably still premature.
- More than about 50 engineers, with teams genuinely blocking each other's releases: micro-frontends begin to pay off, not because the application grew but because the organization did.
Explaining an architecture choice convincingly
Senior roles expect a decision, not a catalog of options. A strong answer tends to have four parts:
- What you run. For example: marketing pages are statically generated, dashboards use SSR, and a BFF sits in front of the mobile app.
- Why. Static generation gives a very fast first paint for content that rarely changes; SSR lets personalized data, such as the user's name, appear without a flash of incorrect content.
- The cost you chose to pay. The BFF introduces an extra hop, yet it gives the frontend team ownership of its data contract, which justified it.
- What you rejected and why. Micro-frontends were evaluated, but the coordination overhead did not justify itself at the current team size.
The last point carries the most weight. Explaining what you deliberately did not choose is what separates reciting a diagram from making a decision. The edge rendering reversal is a ready-made illustration: even the platform that championed the approach changed course when measurements disagreed.
Common questions
How do SSR and SSG differ?
Because SSR renders on every request, it can include live or per-user data. SSG builds HTML once during the build and serves static files, which is faster and cheaper but only as fresh as the latest deployment. ISR sits between them by regenerating individual pages on a timer.
Is a BFF worth it with only one frontend?
Usually not. A BFF earns its place when several clients, such as mobile, web and a partner API, need substantially different payloads from one backend. With a single frontend it mostly adds a network hop and another service to operate.
Is edge rendering dead?
No, but the default has changed. Serving static shells from the edge is still valuable, and edge platforms like Cloudflare Workers shine when data is globally distributed. For typical apps backed by a single-region database, the rule of thumb is now to place compute near the data rather than near the user.
When should a team move to micro-frontends?
When release coordination between teams becomes the actual bottleneck, and not before. A complex application owned by a single team gets the same organizational benefits from a modular monolith at a fraction of the overhead.
Key takeaways
- Every pattern here is an answer to one question: how much work belongs to the browser, the server and the build step.
- The right position depends on team size, where your data lives and how painful deployments are, far more than on which pattern sounds most impressive.
- BFFs, micro-frontends and edge rendering all trade operational cost for a specific benefit; name that trade-off explicitly.
- Keep the reasoning behind rejected options ready. It is the most convincing evidence that a choice was deliberate.