cf.completefrontend
My progress
FUNDAMENTALS · MODULE 7

Choosing a rendering strategy

How to pick the rendering strategy for each page by asking who sees it, how often it changes, how interactive it is, and whether strangers must find it, then mixing strategies route by route in one app instead of choosing one for everything.

The problem: one app, many kinds of pages

Imagine a team building an online shop. Early on they pick one approach for everything: a single-page app rendered in the browser, because the checkout and the account pages need a lot of interactivity. It ships fast and it feels good to use once it is loaded.

Six months later the growth team brings bad news. Product pages rank poorly in search, links shared in chat apps show a blank preview, and shoppers on mid-range phones see a white screen for two to three seconds after tapping an ad. The marketing site, built in the same app, has the same problems.

So the team flips the whole app to server-side rendering. Search and previews recover. Then the server bill triples, the account dashboard gets slower because every click now waits for a server, and the help center, which changes once a week, is rebuilt from scratch for every visitor.

The real mistake

Neither choice was wrong in itself. The mistake was choosing one strategy for the whole app. A blog post, a product page, a checkout, and an analytics dashboard have different needs, and the best rendering strategy is a property of a page, not of a company or a framework.

This lesson ties together the strategies covered in the earlier lessons of this module: "Client-side rendering", "Server-side rendering", "Static generation and ISR", "Streaming SSR", "Islands and partial hydration", and "React Server Components". It does not re-teach them. It teaches you how to choose between them, page by page, and how to defend the choice.

What you will be able to do

  • Ask four questions about any page and arrive at a strategy with a reason, not a habit.
  • Place common page types: marketing and blog pages, docs, product listing and product pages, cart and checkout, dashboards behind login, social feeds, and editors.
  • Mix strategies per route in one app, and inside one page, without making the system hard to run.
  • Judge edge rendering: when running code close to the user helps, and when it makes the page slower because the data is far away.
  • Compare strategies on one table: first byte, largest paint, input delay, JavaScript shipped, server cost, cacheability, freshness, and complexity.

The mental model: when and where the HTML is built

Every rendering strategy answers one question: when, and where, does the HTML for this page get built? Once you see the strategies that way, they stop being a list of buzzwords and become points on a map.

Three moments, three places

  • At build time, once. Static generation (SSG) builds the HTML before anyone asks for it and stores it on a CDN. ISR, incremental static regeneration, is the same idea with a refresh: the stored page is rebuilt after a timer or when content changes.
  • At request time, on a server. SSR builds the HTML for each request. Streaming SSR sends it in pieces as the data arrives. The server can sit in one region near the database, or at the edge near the user.
  • At run time, in the browser. CSR sends an almost empty page and lets JavaScript build everything on the user's device.

Islands and React Server Components are not a fourth moment. They answer a different question: once the HTML exists, how much of the page needs JavaScript in the browser? Islands hydrate only the interactive parts. Server Components keep non-interactive components out of the bundle entirely. Both sit on top of SSR or SSG.

An everyday comparison

Think of a bakery. Bread baked at dawn and set on the shelf is SSG: instant to hand over, the same loaf for everyone, a little less fresh by evening. A cake decorated with your name while you wait is SSR: made for you, but you wait at the counter.

A baking kit you take home is CSR: the shop is fast, but you do the work in your own kitchen. A good bakery sells all three, and nobody asks it to pick one.

The four questions

Four questions settle most pages. Ask them in this order, because the first answers rule out the most options.

  1. Must strangers find it and share it? If search engines and link previews matter, the content must be in the HTML response. That rules out pure CSR for the page.
  2. Is it the same for everyone? If yes, the HTML can be built once and cached at a CDN (SSG, ISR, or cached SSR). If it holds one user's data, it cannot be shared from a cache as a whole.
  3. How fresh must it be? Content that changes weekly suits SSG. Content that changes every few minutes suits ISR or SSR with a short cache. Prices, stock, and inboxes that must be correct to the second need request-time data.
  4. How interactive is it? A page people read once wants little JavaScript: islands or Server Components. A tool people use for an hour wants a rich client: CSR after the first load is fine, because the startup cost is paid once.
A decision flowchart. First question: must strangers find it? If no, client-side rendering, for pages behind a login. If yes, is it the same for everyone? If no, server rendering, streamed, with a private cache. If yes, how fresh must it be? Changes on publish: static generation. Changes within minutes: ISR or cached SSR. Changes within seconds: a cached shell with live holes. A note below: then ask how interactive it is; reading pages get islands or server components, tools get a full client app.Found by strangers?CSRbehind a loginSame for all?SSR, streamedprivate, no-storeHow fresh?SSGon publishISR, cached SSRminutesShell + holessecondsnoyesnoyesThen ask: how interactive is it?Reading page: islands or RSC. Tool: full client app.
Ask the questions in this order, because the first answers rule out the most options. The last question, interactivity, does not change where the HTML is built; it decides how much JavaScript the page ships.

Two more questions break ties, and they come up in real teams more than in textbooks: what can we host and run? and what does the team already know? A team with only static hosting cannot do request-time SSR. A team with deep React and Node.js skills will ship a Next.js app faster than an Astro site. Chapter 8 covers both.

Check your understanding · Exercise 1

A company's pricing page is public, the same for every visitor, changes about once a month, and has one interactive toggle for monthly or yearly prices. Which strategy fits best? Pick one.

The strategies side by side

Each earlier lesson measured its own strategy. To choose, you need them on one scale. This chapter puts one product page through every strategy with the same phone, the same network, and the same bundle, then compares them on eight properties.

The model page

The page is a product page with a title, a price, a hero photo, and an "Add to cart" button, opened on a mid-range Android phone over 4G. The full app bundle is 250 KB compressed, about 900 KB unzipped, and the APIs answer in 120 ms when called from a server in their own region. Content counts as visible when the hero photo paints, about 500 ms after the HTML arrives.

The network borrows from two earlier lessons. Round trips take 80 ms, as in "Server-side rendering", so connecting and sending the request cost about 240 ms, and the first bytes match that lesson: 250 ms for a static file, 400 ms for SSR. Downloads run at about 200 KB per second, the Slow 4G speed from "Client-side rendering".

We also use the rule of thumb from the estimation lesson: about 1 second of main-thread work per MB of unzipped JavaScript on a mid-range phone. So the full bundle costs about 900 ms to parse and run, whether that run is a CSR render or a hydration. These numbers come from a simple model, not a trace.

Because the model mixes two settings, its totals fall between those lessons. The CSR walk-through, with 150 ms round trips, reaches LCP at about 3.4 s; the SSR lab, with a faster download, shows content and interactivity sooner. The shape is the same in all three, and it matches what real-user data shows.

One product page under each strategy, in the model above. TTFB is the time to the first byte of the response; LCP, largest contentful paint, is when the biggest element appears.
StrategyFirst byte (TTFB)Content visible (LCP)UsableWhat sets the pace
CSRabout 250 msabout 2,750 msabout 2,750 msDownload and run the bundle, then call the API
SSRabout 400 msabout 900 msabout 2,550 msData and render before the first byte; hydration after
SSG or ISR from the CDNabout 250 msabout 750 msabout 2,400 msNothing before the first byte; hydration after
Streaming SSRabout 280 msabout 800 msabout 2,430 msShell first; slow sections stream in later
SSG or SSR with islands or RSC (60 KB)250 to 400 ms750 to 900 msabout 770 to 920 msOnly the 60 KB of interactive parts download and run

Three patterns stand out. CSR has a fast first byte and the slowest everything else, because the content waits for the bundle and the API call. The server strategies show content within a second, but the full bundle still has to run before the page responds. Only cutting the JavaScript, with islands or Server Components, closes the gap between visible and usable.

Five timelines for one product page on a mid-range phone over 4G. CSR: HTML at 250 ms, blank until 2.75 s, then interactive. SSR: HTML at 400 ms, content at 900 ms, interactive at 2.55 s. SSG or ISR from the CDN: HTML at 250 ms, content at 750 ms, interactive at 2.4 s. Streaming SSR: shell at 280 ms, content at 800 ms, a slow section at 1.2 s, interactive at about 2.43 s. Islands or server components: content at 750 ms, interactive at about 770 ms.blanklooks readyinteractiveHTML arrivesCSRSSRSSG or ISRStreamingIslands, RSC0s0.5s1s1.5s2s2.5s3s
Hatched is a blank screen, warm is a page that looks ready but ignores taps, green is interactive. The server strategies move content earlier; only shipping less JavaScript shrinks the warm stretch.

Try the simulator below. Raise the bundle size and watch the "usable" bar grow for every strategy except islands. Raise the API time and watch SSR's first byte move while SSG stays put. The point to take away: different inputs hurt different strategies, so the right choice depends on which input your page has the most of.

CSR: first byte 250 ms, content 2750 ms, usable 2750 ms. SSR: first byte 400 ms, content 900 ms, usable 2550 ms. SSG or ISR: first byte 250 ms, content 750 ms, usable 2400 ms. Streaming: first byte 280 ms, content 800 ms, usable 2430 ms. Islands, RSC: first byte 250 ms, content 750 ms, usable 766 ms.

Five timelines from the inputs above. CSR: first byte 250 ms, content 2750 ms, usable 2750 ms. SSR: first byte 400 ms, content 900 ms, usable 2550 ms. SSG or ISR: first byte 250 ms, content 750 ms, usable 2400 ms. Streaming: first byte 280 ms, content 800 ms, usable 2430 ms. Islands, RSC: first byte 250 ms, content 750 ms, usable 766 ms.blanklooks readyinteractiveHTML arrivesCSRSSRSSG or ISRStreamingIslands, RSC0s0.5s1s1.5s2s2.5s3s
The lesson's model: 80 ms round trips, 200 KB per second, and 1 second of main-thread work per MB of unzipped JavaScript. Hatched is blank, warm looks ready but ignores taps, green is usable; the tick is the first byte and the dot is a slow section streaming in.

Eight properties, one table

Speed is three of the properties that matter. The other five are about money, freshness, and how hard the thing is to run. INP, interaction to next paint, is how quickly the page responds to a tap; it suffers when long JavaScript tasks block the main thread.

The comparison to carry into an interview. Columns are strategies; rows are what you care about.
PropertyCSRSSRSSG and ISRStreaming SSRIslands or RSC
TTFBLow: static shellHigher: waits for dataLowest: CDN fileLow: shell firstSame as the base it sits on
LCPLate: after JS and APIEarlyEarliestEarly if the LCP element is in the shellEarly
INPGood after load; bad duringRisk during hydrationRisk during hydrationBetter: selective hydrationBest: little to hydrate
JS shippedWhole appWhole appWhole app, unless islandsWhole appInteractive parts only
Server cost per viewNear zeroHighest: render per requestNear zeroHigh, like SSRSame as the base
CacheabilityShell yes, data per userOnly if not personalFull, at the CDNShell can be cachedSame as the base
FreshnessLiveLiveBuild or timer delayLiveSame as the base
ComplexityLowMedium: servers to runLow to medium: builds, invalidationMedium to high: boundaries, bufferingMedium: new boundaries to learn

Check your understanding · Exercise 2

An SSR product page shows content at 0.9 s, but taps on "Add to cart" do nothing until 2.5 s. Which change narrows that gap the most? Pick one.

A decision guide by page type

Most products are built from a handful of page types that repeat across companies. Learn the usual answer for each, and why, and you can reason about any new page by comparing it with the closest one.

A matrix of eight page types against five strategies: CSR, SSR, SSG or ISR, a cached shell with holes, and islands or server components. Marketing, blog: SSG, ISR and Islands best. Docs: SSG, ISR and Islands best. Product listing: SSR and SSG, ISR best. Product detail: Shell and Islands best. Cart, checkout: CSR and SSR best. Dashboard: CSR best. Social feed: SSR best. Editor: CSR best.CSRSSRSSG, ISRShellIslandsMarketing, blogpoorokbestokbestDocspoorokbestokbestProduct listingpoorbestbestokokProduct detailpoorokokbestbestCart, checkoutbestbestpoorokokDashboardbestokpoorokpoorSocial feedokbestpoorokokEditorbestpoorpoorpoorpoorbest: usual choice · ok: works with care · poor: fights the page
Read across a row to see which strategies fit a page. Shell means a cached shell with dynamic holes. Content pages lean right, toward early HTML and little JavaScript; logged-in tools lean left, toward the client. Islands describe how much JavaScript ships, so they combine with SSG, SSR, or a shell.

Marketing pages and blog posts

These pages are public, the same for everyone, and read once. Search ranking and link previews are the whole point. Content changes when someone publishes, a few times a day at most. That makes them the textbook case for static generation, with on-demand revalidation so a published fix goes live in seconds rather than at the next full build.

The interactive parts are small: a newsletter form, a menu, maybe a video player. Ship them as islands or client components and keep the rest as plain HTML. A blog post should load less than 50 KB of JavaScript; many ship close to none.

Documentation

Docs look like a blog with more pages and more structure. Static generation still wins: the content changes with releases, and readers arrive from search. The interactive parts are search, a version switcher, and code copy buttons, all islands.

The catch is scale. Ten versions of 2,000 pages is 20,000 pages per build. Build only the latest version fully and generate older ones on first request, then cache them. The "Static generation and ISR" lesson covers build times at that size.

Product listing pages

A category page, such as "running shoes", is public, ranked in search, and shared. It changes when stock and prices change, which can be hourly. Filters and sorting multiply the number of URLs: 30 brands by 12 sizes by 4 sort orders is already 1,440 variants of one category.

So build the plain category page with ISR on a short timer (say 5 minutes) or as SSR cached at the CDN with stale-while-revalidate. Keep filters in the URL query so each variant is its own cacheable page, and let rare combinations render on demand. Anything personal, like "recommended for you", loads in the browser after the shared page arrives.

Product detail pages

A product page is where search traffic lands and where speed costs money. Most of it, the title, photos, and description, is the same for everyone and changes rarely. A few parts change fast or belong to one user: price, stock, delivery estimate, and the cart count.

Split it. Serve the stable part as ISR or cached SSR from the CDN, so LCP is fast and the HTML is complete for crawlers. Fetch the fast-changing or personal parts separately: from the browser after load, or as dynamic holes streamed into a cached shell (chapter 5 shows how). The "Add to cart" button is a small interactive island.

Cart and checkout

These pages are personal, never indexed, and must be correct to the cent. Caching shared HTML is wrong here, so either SSR without a shared cache (Cache-Control: private, no-store) or CSR from a static shell works. The visitor is already in the app, often with the code cached from the product page, so CSR's startup cost is smaller than on a first visit.

Checkout adds one more rule: reliability beats cleverness. Keep third-party scripts to the payment provider, make the forms real form elements that submit even if a script fails, and render validation errors on the server as well as in the browser.

Dashboards behind a login

An analytics dashboard or an admin tool has no SEO need, is fully personal, and stays open for hours. The startup cost is paid once and spread over a long session. CSR from a CDN-hosted shell is a fine default, and it keeps hosting simple: no rendering servers at all.

Move to SSR or streaming only when the first view matters a lot, for example a dashboard people open from an email link many times a day. Then stream the shell and the navigation at once and let each slow chart arrive in its own boundary.

Social feeds

A feed is personal, changes every second, and is very interactive. The signed-in home feed is usually rendered on the server for the first screen (often streamed), so posts appear fast, and then the client takes over: infinite scroll, likes, and new-post banners all run in the browser.

The public parts of a social product are different pages with different answers. A single post or a profile, opened from a shared link by someone not signed in, must render on the server with its Open Graph tags, and can be cached for a short time because it is the same for every visitor.

Editors and canvas tools

A document editor, a design tool, or a spreadsheet is almost all interaction, with heavy state in the browser and often a canvas. Server-rendering the editing surface buys little: it must hydrate everything before anyone can type. CSR is the answer, with the marketing pages and a read-only share view built separately as SSG or SSR.

PageSEO and previewsSame for everyoneFreshnessInteractivityUsual choice
Marketing, blogCriticalYesOn publishLowSSG plus islands, on-demand revalidation
DocsCriticalYesOn releaseLow to mediumSSG, older versions on demand
Product listingHighMostlyMinutesMedium (filters)ISR or cached SSR, filters in the URL
Product detailHighMostlyPrice and stock: secondsMediumCached shell plus dynamic holes and islands
Cart, checkoutNoneNoSecondsHighUncached SSR or CSR
DashboardNoneNoLiveHigh, long sessionsCSR, or streaming SSR for the first view
FeedPublic posts onlyNoLiveHighSSR first screen, then client
EditorNoneNoLiveVery highCSR; marketing and share views separate

Now try it yourself. The lab below takes the four questions as inputs and recommends a strategy with its reasons. Describe a page you know, then change one answer at a time and see which input moves the recommendation.

Search and link previews
Personalization
Content changes
Interactivity

Recommended: ISR, or SSR cached at the CDN with stale-while-revalidate

  • Content changes too often for builds but not for every request.
  • One copy per URL serves everyone.
  • Visitors get a cached copy at once while a fresh one renders in the background.

JavaScript: Hydrate only the widgets, as islands or client components at the leaves of the tree.

Watch out: Pick the cache lifetime from how stale the content may be, and revalidate on change.

Check your understanding · Exercise 3

Links to single posts on a social app show an empty preview card in chat apps. The whole app is client-rendered. What is the smallest fix that solves it? Pick one.

Mixing strategies in one app

The answer to "which strategy?" is almost never one word. Modern frameworks let you set the strategy per route, and some let you mix them inside one page. This chapter shows both levels, and the one shared piece that most often ruins a mix.

Per route: a strategy map

Draw the app as a tree of routes and write a strategy on each branch. Children usually inherit from their parent, and you override where a page differs. The figure shows the shop from chapter 1.

A route tree for an online shop. The home page is static with revalidation. Under it: /blog is static, cached at the CDN. /products uses ISR every 5 minutes, cached at the CDN with dynamic holes. /cart is server-rendered and private. /checkout is server-rendered with no-store. /app, the account area, is client-rendered from a static shell. A note: render the avatar in the shared layout as a hole, or every route becomes dynamic./ (home)SSG + revalidate/blogSSGCDN/productsISR 5 minCDN + holes/cartSSRprivate/checkoutSSRno-store/appCSRstatic shellShared layout: render the avatar as a hole,or every route becomes dynamic.
One app, five strategies, one per branch, each with its own cache rule. The shared layout above all of them is where mixing most often breaks.

Frameworks express this with a few lines per route. In SvelteKit, each page can export options that say whether to build it at build time, render it on the server, or skip server rendering.

ts
// src/routes/blog/[slug]/+page.ts
// Built once at build time, served as a static file.
export const prerender = true;

// src/routes/products/[id]/+page.server.ts
// Rendered on the server per request; the CDN caches it briefly.
import type { PageServerLoad } from "./$types";

export const load: PageServerLoad = async ({ params, setHeaders }) => {
  setHeaders({ "cache-control": "public, s-maxage=300, stale-while-revalidate=3600" });
  return { product: await getProduct(params.id) };
};

// src/routes/app/+layout.ts
// The signed-in dashboard: no server render, the browser does it all.
export const ssr = false;
SvelteKit page options, one file per route. Astro has the same idea with export const prerender.

Next.js does the same with its own vocabulary: static routes by default, generateStaticParams for prebuilt dynamic paths, cache lifetimes for revalidation, and "use client" components for browser-only parts. The names change between frameworks and versions; the map you draw does not.

Inside one page: a shell with holes

A product page is mostly shared and partly personal. Instead of making the whole page dynamic because of one part, build a shared shell and fill the holes separately. There are three ways to fill a hole.

  • From the browser after load. The cached page has a placeholder; a small script fetches /api/stock/42 and fills it. Simple and works anywhere, but the hole fills only after that script runs, and reserved space is needed to avoid layout shift.
  • Streamed from the server. The shell is prerendered and sent from the cache at once, and the server streams the dynamic parts into the same response. Next.js calls this Partial Prerendering. The hole fills sooner, with no extra request from the browser.
  • Stitched at the CDN. Edge Side Includes (ESI) or an edge function combine a cached page with a small personal fragment. ESI is older and less common in modern stacks, but CDNs such as Akamai and Fastly still support it.
A wireframe of a product page. The cached shell holds the header with logo and search, the product photo, the title Trail Runner 2, and the description. Dynamic holes, filled per request, are the avatar in the header, the price and stock box, and the recently viewed row. The Add to cart button is an island that ships JavaScript.shop.example/p/42logo · searchavatarTrail Runner 2price, stockAdd to cartdescription, specsrecently viewedcached shelldynamic holeisland (JS)
Most of the page is the same for everyone and comes from the cache. Only the dashed boxes are per request, and only the green button needs JavaScript. Reserve each hole's size so nothing shifts when it fills.

The trap: a personal header on every page

Most apps share one layout: a header with the logo, a search box, and the user's avatar and cart count. If the server renders that avatar into the HTML, every page that uses the layout becomes personal, and none of them can be cached at the CDN. Teams discover this when the blog, which should cost nothing, shows up as a large line on the server bill.

The fix is to render the header's shared part on the server and make the personal part a hole: a small client component that reads the session and fills in the avatar, or a streamed dynamic part. The page stays cacheable; only a few hundred bytes are personal.

Moving between parts of the app

Mixing has a cost at the seams. If the marketing site is an Astro build and the app is a separate React SPA, going from one to the other is a full page load, and the app's bundle starts cold. That is usually fine: people cross that seam once per visit. Inside one framework, client-side navigation smooths the seams, which is the subject of the next lesson, "SPA vs MPA".

Check your understanding · Exercise 4

A team set its blog to static generation, but monitoring shows the blog routes are rendered on the server for every request. Which is the most likely cause? Pick one.

Personalization against caching

The biggest single force in choosing a strategy is personalization, because it decides whether the HTML can be cached. A CDN can serve a page from a nearby city in 20 to 40 ms, but only if many people get the same bytes. Each bit of personal data in the HTML shrinks the group that can share it.

Three levels of personal

  • Not personal. Every visitor gets the same page: a blog post, a docs page. One cached copy per URL. Hit ratios above 95 percent are normal.
  • Personal by segment. The page depends on a few coarse facts: country, language, currency, or an A/B test bucket. 20 countries by 2 test buckets is 40 copies of each page, still very cacheable.
  • Personal by user. The page shows this user's name, cart, or feed. There are as many versions as users, so a shared cache cannot help with the whole page.

The goal is to push as much of each page as possible up this list. A product page that is "personal by user" because of a cart badge can become "not personal" with one hole, and that change can cut its server cost by more than ten times.

Before and after. Left, the avatar is rendered into the HTML: three visitors, Ana, Ben, and Cy, each miss the CDN cache because the page varies by cookie, so the origin renders the page three times. Right, the avatar is a hole filled in the browser: the page is public with s-maxage of 300 seconds, the first visitor misses and the next two hit, so the origin renders once, and each browser makes a small request for its own avatar.Before: avatar in the HTMLAnaBenCyCDNMISS x 3origin3 rendersVary: CookieAfter: avatar as a holeAnaBenCyCDN1 MISS, 2 HITorigin1 renderpublic, s-maxage=300plus GET /api/me per browser
The page is the same; only where the avatar is rendered changed. With the avatar in the HTML, the cache needs Vary: Cookie and stores one copy per user. As a hole, one shared copy serves everyone.

The headers that decide

  • Cache-Control: public, s-maxage=300 lets shared caches like the CDN keep the response for 5 minutes. Use it only for HTML that is the same for every visitor in its cache key.
  • Cache-Control: private, no-store keeps personal HTML out of every shared cache. Use it on anything with user data.
  • Vary tells caches which request headers change the response. Vary: Accept-Language is fine with a few languages. Vary: Cookie usually means one copy per user, which defeats the cache.

Decision A product page needs the user's cart count and a "recently viewed" row. How do you render it?

SSR per user, no shared cache

Render the whole page for each request with the user's data.

Cache-Control: private, no-store.

Pros

  • Everything is in the first response
  • Simple to reason about

Cons

  • Every view costs a render
  • TTFB includes the slowest personal call
  • Crawlers and anonymous users pay for personal work they do not use

Cached shared page, personal parts from the browser

Serve the shared page from the CDN.

A small client component fetches the cart count and the row after load.

Pros

  • Fast LCP from the cache
  • Server cost near zero for the page
  • Works with any hosting

Cons

  • Personal parts appear later
  • Space must be reserved to avoid layout shift
  • One extra request

Cached shell with streamed holes

Prerender the shared shell.

Stream the personal parts into the same response from the server.

Pros

  • Fast first paint and personal content in one response
  • No extra browser request

Cons

  • Needs a framework and host that support it
  • Every view still starts a server render for the holes

What we pick: Use the cached page with personal parts from the browser as the default; it works everywhere and keeps the page cacheable. Choose streamed holes when the personal part is important to the first view, for example a price that depends on the user's membership. Render per user only when almost the whole page is personal, as on a cart.

Check your understanding · Exercise 5

Edge rendering: close to the user, far from the data

Edge rendering means running your server rendering code in the CDN's points of presence (PoPs), the hundreds of small data centres near users, instead of in one region. The pitch is simple: the HTML is built 10 ms from the user instead of 250 ms away. The catch is just as simple, and the "DNS, CDNs, and the edge" lesson named it: your data is not at the edge.

What runs at the edge

Edge platforms such as Cloudflare Workers, Vercel's edge runtime, Netlify Edge Functions, and Deno Deploy run JavaScript in lightweight isolates with Web APIs like fetch, Request, and Response. They start in milliseconds, but they come with limits: tight CPU time per request, a size limit on the code you deploy, and often no full Node.js API. Heavy rendering of a large page can hit those limits, and Vercel itself now recommends its Node.js runtime over the edge runtime for most code.

Following the data

Take the same reader in Sydney and a database in Virginia, with a 230 ms round trip between them. The product page needs three queries in a row: the product, then its variants, then reviews for the variant shown. Rendering takes 40 ms either way.

  • Regional SSR in Virginia, behind the CDN. The user's connection ends at the Sydney PoP (two 10 ms handshakes, one 10 ms request). The PoP forwards over a warm connection to Virginia: one 230 ms round trip. The three queries cost about 1 ms each next to the database. TTFB is about 30 + 230 + 3 + 40, so roughly 300 ms.
  • Edge SSR in Sydney. The handshakes and request still cost about 30 ms. Then each of the three queries crosses the ocean: 3 x 230 = 690 ms. TTFB is about 30 + 690 + 40, so roughly 760 ms. Twice as slow, with code that is closer to the user.
  • Edge SSR with data at the edge. If the product data is replicated into an edge key-value store or a nearby read replica, each read costs a few milliseconds. TTFB drops to about 30 + 15 + 40, roughly 85 ms. The price is that the data may be seconds to minutes stale.
A sequence diagram with three lifelines: a browser in Sydney, the Sydney edge PoP, and a database in Virginia. The browser sends GET /p/42 to the PoP, about 30 ms with connection setup. The edge function sends query 1 to Virginia and waits 230 ms for the rows, then query 2, another 230 ms, then query 3, another 230 ms. After rendering for 40 ms it sends the HTML: TTFB about 760 ms. A note: regional SSR in Virginia pays one 230 ms trip and three 1 ms queries, about 300 ms.Browser, SydneyEdge PoP, SydneyDB, VirginiaGET /p/42 (30 ms)query 1: productrows, 230 ms laterquery 2: variantsrows, 230 ms laterquery 3: reviewsrows, 230 ms laterHTML at 760 msRegional SSR in Virginia: one 230 ms trip,three 1 ms queries: TTFB about 300 ms
Every query from the edge crosses the ocean, so the long round trip is paid three times. A server next to the database pays it once, on the way back through the CDN.

The rule that falls out: edge rendering helps when the data the page needs is also at the edge, or when the page needs no data at all. With one far-away query, it is roughly a draw. With several queries in a row to a single-region database, it loses, often badly. Use the calculator to find the break-even point for your own page.

Regional SSR next to the database: TTFB about 303 ms. Edge SSR: about 760 ms. Regional rendering wins by 457 ms.

Two bars. Regional SSR: about 303 ms to the first byte. Edge SSR: about 760 ms. Regional rendering wins by 457 ms.Regional SSR: 303 msEdge SSR: 760 ms
Both include 30 ms from the user to the nearby PoP and 40 ms of rendering. Regional pays the long round trip once; the edge pays it once per query, unless the data is replicated nearby (about 5 ms a read).

Where the edge earns its place

  • Decisions from the request alone: redirects, locale and country routing, A/B bucketing, and auth gates that verify a signed token without a database.
  • Picking a cached variant: choosing which prebuilt page to serve for this country or test bucket, then serving it from the cache.
  • Rendering from replicated or embedded data: feature flags, a product catalogue copied to an edge store, or content baked into the deployment.
  • Assembling a cached shell with small personal pieces: the shell from cache, a fragment computed from a cookie.

Check your understanding · Exercise 6

A team moves its dashboard's SSR to edge functions to help users in Asia. The database stays in one US region and the dashboard runs five queries in a row. What happens to TTFB for users in Asia? Pick one.

SEO, link previews, hosting, and the team

Performance tables make rendering look like a pure speed question. In practice three other forces decide as often: who reads the HTML besides humans, where you can run code, and what your team can operate at 3 a.m.

Search crawlers

Googlebot does run JavaScript: it fetches the HTML, puts the page in a queue, and renders it later with a recent Chrome. But rendering costs Google more than reading HTML, and it can happen later than the first crawl. Content that appears only after a click or a login is never seen, and content loaded on scroll can be missed.

Other search engines vary in how much JavaScript they run. The major AI crawlers, such as OpenAI's GPTBot and Anthropic's ClaudeBot, fetch the HTML and do not run JavaScript at all, as the "Client-side rendering" lesson showed; Google's Gemini, which uses Google's own crawling, is the exception.

So for any page that must be found, the safe rule is: the main content, the title, the links, and the meta tags are in the HTML response. SSG, ISR, and SSR give you that. CSR does not. Serving prerendered HTML only to bots, called dynamic rendering, is a workaround Google itself no longer recommends; render the real page on the server instead.

When someone pastes a link into a chat app or a social network, a bot fetches the URL and reads a few tags from the head to build the preview card. These bots almost never run JavaScript, so tags set by a client-side router are invisible to them.

The start of an HTML response, line by line, with notes. The title tag becomes the search result title. The meta description becomes the search snippet. The og:title and og:image meta tags become the link preview card's title and image. The canonical link names the one true URL. The last line, an empty root div from a client-rendered page, is marked: bots that do not run JavaScript see nothing there.view-source: /p/42<title>Trail Runner 2 | Shop</title><meta name="description" content="…"><meta property="og:title" content="…"><meta property="og:image" content="…"><link rel="canonical" href="…"><div id="root"></div>search titlesearch snippetpreview titlepreview imageone canonical URLCSR: empty for bots
Crawlers and link preview bots read these tags straight from the response. If a client-side router sets them later, or the body is an empty div, most bots never see the content.

This is why even CSR-heavy products keep server rendering for their shareable routes: a post, a profile, a product, an invite link. It is often the only server-rendered part of an otherwise client-rendered app.

Hosting decides what is possible

Each strategy needs something to run. Match the plan to what you can host, or plan to change the hosting.

HostingWhat it can runGood forWatch out for
Static files on a CDN (object storage plus CDN, GitHub Pages)Files onlySSG, CSR shellsNo request-time code; previews need prebuilt pages
Serverless functionsSSR per request, scaled to zeroSpiky traffic, small teamsCold starts; per-invocation cost at high volume; some limit streaming
Long-running Node.js servers or containersSSR, streaming, websocketsSteady high trafficCapacity planning, on-call, slow memory leaks
Edge functionsLight SSR and middleware in every PoPRouting, variants, edge dataCPU and size limits, far from the database
Four layers from top to bottom. The browser runs CSR rendering and hydrates islands. The CDN and edge, in hundreds of PoPs, serve SSG and ISR files and run edge routing. The region near the data runs SSR with streaming and renders server components. The data layer holds the database and the APIs. Arrows show HTML flowing up from the region through the CDN to the browser.BrowserCSR renderhydrate islandsCDN, edgeSSG, ISR filesedge routingRegionSSR, streamingRSC renderDatadatabaseAPIs
Each strategy is a choice of layer for the rendering work. Static files and routing belong at the edge; anything that talks to the database belongs in the region next to it; the browser does what depends on the user's device and session.

A quick server cost estimate

Do the arithmetic before you choose request-time rendering for a busy page. Say a product page gets 2,000 requests per second at peak and takes 50 ms of CPU to render. That is 2,000 x 0.05 = 100 CPU cores busy, just for rendering, before any headroom.

With a 90 percent CDN hit ratio, the origin sees 200 requests per second and 10 cores are enough. Caching changes the bill by ten times; the framework choice barely does.

The team

  • Skills. A team that knows React and Next.js will ship SSR with Server Components faster than it will learn a new islands framework, and the reverse is true for a content team on Astro.
  • Operations. Request-time rendering means servers to watch, scale, and wake up for. Static output and CSR mean a build pipeline and a CDN, which fail less often and more simply.
  • Debugging. Hydration mismatches, streaming buffers, and cache bugs need people who understand them. Pick a strategy whose failures your team can diagnose.

Check your understanding · Exercise 7

A news homepage gets 5,000 requests per second during a big event. SSR costs 40 ms of CPU per render. The CDN caches it for 30 seconds. Roughly how much rendering does the origin do? Pick one.

Signs of the wrong choice, and how to change course

Rendering choices are rarely wrong on day one. They become wrong as the product changes: a public page gains a personal widget, a dashboard gets shared links, traffic grows ten times. These are the symptoms to watch for, what usually causes them, and the smallest fix.

SymptomLikely causeSmallest fix
Poor rankings, blank link previewsPublic pages rendered only in the browserServer-render or prerender those routes; keep the rest CSR
LCP of 3 s or more on mobile, good TTFBCSR waterfall: HTML, then JS, then dataMove the page's content into the HTML, or at least preload the data
Good LCP, poor INP, taps ignored after loadHydrating the whole app bundleIslands or Server Components; split and defer non-critical code
High p95 TTFB on SSR pagesSerial data calls, a slow dependency, server far from dataFetch in parallel, add timeouts, stream slow parts, render near the data
Server bill grows with traffic on content pagesShared pages not cached, often a personal headerCache shared HTML at the CDN; make personal parts holes
Users report stale prices or stockLong ISR timer or cache lifetime on fast dataFetch volatile fields at request time or from the client; revalidate on change
A user sees another user's dataPersonal HTML in a shared cacheprivate, no-store on personal routes; purge; add a two-user test
Builds take 40 minutesPrerendering every page at build timeBuild popular pages; render the long tail on demand and cache it

Measure before you migrate

Changing the rendering strategy of a big app is months of work, so be sure it is the real problem. Field data from real users (the Chrome UX Report or your own real-user monitoring) tells you which metric is failing and on which routes. A slow LCP from a 2 MB hero image is an image problem, not a rendering one.

Migrate route by route

Because strategies mix per route, migrations can too. Move the route that hurts the most first, usually the top landing page, measure it for two weeks, then move the next. Frameworks that support per-route choices, or a CDN rule that sends some paths to a new app, let the old and new strategies live side by side for as long as needed.

In real products

  • Content sites built with Astro prerender pages by default and hydrate only marked components, which is why its sites often ship little or no JavaScript.
  • Next.js apps choose per route: static by default, dynamic when a route reads request data, with client components only where interaction is needed. The DNS, CDN, and edge lesson shows how a typical Vercel deployment spreads that across the CDN and a region.
  • Shopify's Hydrogen framework, built on React Router, renders storefronts on the server with streaming, because product and collection pages live on search traffic and first impressions.
  • Figma and similar canvas tools are client-rendered applications, with the editor drawn by the browser; their marketing and community pages are separate, server-rendered sites.

Think first

Your client-rendered analytics dashboard adds "share this chart" links that anyone with the link can open. What changes in your rendering plan?

Choosing a strategy in a system design interview

Rendering comes up in almost every frontend system design interview, usually right after requirements. The interviewer wants to see that you can tie a choice to the requirements and name its costs, not that you know the newest framework feature.

When to bring it up

Raise it as soon as you have the non-functional requirements: who the users are, whether pages must be found by search, how fresh data must be, and the device and network you design for. Say "let me decide rendering per page" and list the main pages from the requirements.

What to draw

  • A route map with a strategy and a cache rule on each route, like the tree in chapter 5.
  • The page split for the most important page: the cached shell and the holes, like the product page wireframe.
  • Where things run: browser, CDN or edge, regional server, data. One arrow for the cached path and one for the dynamic path.

What to say, in four steps

  1. Ask the four questions out loud for each main page: found by strangers, same for everyone, how fresh, how interactive.
  2. Pick a strategy per page and give the reason in one sentence: "The product page is public and mostly shared, so ISR from the CDN with price and stock as dynamic holes."
  3. Name the cost and its fix: "ISR can show a stale description for up to 5 minutes; we revalidate on publish. Hydration could hurt INP, so the gallery and the cart button are the only client components."
  4. Tie it to numbers: an LCP target (2.5 s at the 75th percentile), a JavaScript budget (say 150 KB compressed for the product page), and a cache hit ratio target.

How deep to go, by level

LevelWhat a strong answer shows
JuniorKnows CSR, SSR, and SSG; picks SSR or SSG for public pages and CSR for the logged-in app; mentions SEO
MidChooses per route; explains TTFB against LCP and INP; caches shared HTML at the CDN and keeps personal data out of it
SeniorSplits a page into a cached shell and holes; weighs edge against regional rendering by data location; budgets JavaScript; plans migration route by route; mentions hosting and team limits

Check your understanding · Exercise 8

You are designing a recipe site with 200,000 recipes, user ratings that change constantly, and a signed-in "my saved recipes" page. Which plan is strongest? Pick one.

Summary

  • Rendering strategy is a property of a page, not of an app: decide per route, and inside a page split a cached shell from personal or fast-changing holes.
  • Four questions settle most pages: must strangers find and share it, is it the same for everyone, how fresh must it be, and how interactive is it.
  • Build as early as you can and run as little JavaScript as you can; move later only when personalization, freshness, or interactivity forces it.
  • Judge with LCP and INP from real users, not TTFB alone: SSR raises TTFB and still cuts LCP, and only less JavaScript closes the gap between visible and usable.
  • Personal HTML cannot live in a shared cache; push pages from per-user to per-segment to shared, and never let a personal header make every page dynamic.
  • Edge rendering helps only when the data is at the edge too; with a single-region database, render next to the data and use the edge for routing and variants.
  • Crawlers and link preview bots read the HTML; any page that must be found or shared needs its content and meta tags in the response.
  • Hosting, cost, and team skills are real constraints: estimate render CPU at peak, count on the cache to cut it, and pick failures your team can debug.

In the interview

Questions interviewers ask about this. Answer each out loud first, then compare.

Question 1

SSR or CSR for this app?

Question 2

The product page shows a personal cart count and a price that changes often. How can it still be cached?

Question 3

Should we render at the edge to make the site faster worldwide?

Question 4

We switched to SSR and LCP improved, but INP got worse. Why, and what would you do?

Question 5

How would you pick rendering strategies for a social network?

Practice it

These design problems lean on choosing a rendering strategy.

References

Read next