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.
- 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.
- 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.
- 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.
- 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.
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.
| Strategy | First byte (TTFB) | Content visible (LCP) | Usable | What sets the pace |
|---|---|---|---|---|
| CSR | about 250 ms | about 2,750 ms | about 2,750 ms | Download and run the bundle, then call the API |
| SSR | about 400 ms | about 900 ms | about 2,550 ms | Data and render before the first byte; hydration after |
| SSG or ISR from the CDN | about 250 ms | about 750 ms | about 2,400 ms | Nothing before the first byte; hydration after |
| Streaming SSR | about 280 ms | about 800 ms | about 2,430 ms | Shell first; slow sections stream in later |
| SSG or SSR with islands or RSC (60 KB) | 250 to 400 ms | 750 to 900 ms | about 770 to 920 ms | Only 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.
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.
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.
| Property | CSR | SSR | SSG and ISR | Streaming SSR | Islands or RSC |
|---|---|---|---|---|---|
| TTFB | Low: static shell | Higher: waits for data | Lowest: CDN file | Low: shell first | Same as the base it sits on |
| LCP | Late: after JS and API | Early | Earliest | Early if the LCP element is in the shell | Early |
| INP | Good after load; bad during | Risk during hydration | Risk during hydration | Better: selective hydration | Best: little to hydrate |
| JS shipped | Whole app | Whole app | Whole app, unless islands | Whole app | Interactive parts only |
| Server cost per view | Near zero | Highest: render per request | Near zero | High, like SSR | Same as the base |
| Cacheability | Shell yes, data per user | Only if not personal | Full, at the CDN | Shell can be cached | Same as the base |
| Freshness | Live | Live | Build or timer delay | Live | Same as the base |
| Complexity | Low | Medium: servers to run | Low to medium: builds, invalidation | Medium to high: boundaries, buffering | Medium: 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.
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.
| Page | SEO and previews | Same for everyone | Freshness | Interactivity | Usual choice |
|---|---|---|---|---|---|
| Marketing, blog | Critical | Yes | On publish | Low | SSG plus islands, on-demand revalidation |
| Docs | Critical | Yes | On release | Low to medium | SSG, older versions on demand |
| Product listing | High | Mostly | Minutes | Medium (filters) | ISR or cached SSR, filters in the URL |
| Product detail | High | Mostly | Price and stock: seconds | Medium | Cached shell plus dynamic holes and islands |
| Cart, checkout | None | No | Seconds | High | Uncached SSR or CSR |
| Dashboard | None | No | Live | High, long sessions | CSR, or streaming SSR for the first view |
| Feed | Public posts only | No | Live | High | SSR first screen, then client |
| Editor | None | No | Live | Very high | CSR; 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.
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.
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.
// 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;
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/42and 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.
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.
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=300lets 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-storekeeps personal HTML out of every shared cache. Use it on anything with user data.Varytells caches which request headers change the response.Vary: Accept-Languageis fine with a few languages.Vary: Cookieusually 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?
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.
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.
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.
Link previews
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.
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.
| Hosting | What it can run | Good for | Watch out for |
|---|---|---|---|
| Static files on a CDN (object storage plus CDN, GitHub Pages) | Files only | SSG, CSR shells | No request-time code; previews need prebuilt pages |
| Serverless functions | SSR per request, scaled to zero | Spiky traffic, small teams | Cold starts; per-invocation cost at high volume; some limit streaming |
| Long-running Node.js servers or containers | SSR, streaming, websockets | Steady high traffic | Capacity planning, on-call, slow memory leaks |
| Edge functions | Light SSR and middleware in every PoP | Routing, variants, edge data | CPU and size limits, far from the database |
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.
| Symptom | Likely cause | Smallest fix |
|---|---|---|
| Poor rankings, blank link previews | Public pages rendered only in the browser | Server-render or prerender those routes; keep the rest CSR |
| LCP of 3 s or more on mobile, good TTFB | CSR waterfall: HTML, then JS, then data | Move the page's content into the HTML, or at least preload the data |
| Good LCP, poor INP, taps ignored after load | Hydrating the whole app bundle | Islands or Server Components; split and defer non-critical code |
| High p95 TTFB on SSR pages | Serial data calls, a slow dependency, server far from data | Fetch in parallel, add timeouts, stream slow parts, render near the data |
| Server bill grows with traffic on content pages | Shared pages not cached, often a personal header | Cache shared HTML at the CDN; make personal parts holes |
| Users report stale prices or stock | Long ISR timer or cache lifetime on fast data | Fetch volatile fields at request time or from the client; revalidate on change |
| A user sees another user's data | Personal HTML in a shared cache | private, no-store on personal routes; purge; add a two-user test |
| Builds take 40 minutes | Prerendering every page at build time | Build 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?
The dashboard itself can stay CSR: it is personal and used in long sessions. The new shared-chart route is different: it is opened by people who have never loaded the app, often from a chat message, and it needs a preview card.
So render that one route on the server, with Open Graph tags and a static image of the chart for the preview, and make it read-only with a small island for hovering on data points. Cache it per share id with a short lifetime, and keep the full editing app behind the login as before.
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
- Ask the four questions out loud for each main page: found by strangers, same for everyone, how fresh, how interactive.
- 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."
- 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."
- 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
| Level | What a strong answer shows |
|---|---|
| Junior | Knows CSR, SSR, and SSG; picks SSR or SSG for public pages and CSR for the logged-in app; mentions SEO |
| Mid | Chooses per route; explains TTFB against LCP and INP; caches shared HTML at the CDN and keeps personal data out of it |
| Senior | Splits 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?
It depends on the page, so I would decide per route. Public pages that must be found and shared, like products and articles, need their content in the HTML: static generation or cached SSR. Pages behind a login that people use for long sessions, like a dashboard, can be client-rendered from a static shell, which keeps hosting simple.
Then I would name the costs: SSR adds server work and a later first byte, so I cache shared HTML at the CDN; both SSR and CSR ship the whole bundle unless I limit hydration with islands or Server Components.
Question 2
The product page shows a personal cart count and a price that changes often. How can it still be cached?
Split the page into a shared shell and holes. The title, photos, and description are the same for everyone, so they are prerendered or cached at the CDN with a short lifetime and revalidated when the product changes.
The cart count is fetched in the browser or streamed into the response, and price and stock are fetched at request time. The shared HTML never contains personal data, and personal responses use Cache-Control: private.
Question 3
Should we render at the edge to make the site faster worldwide?
Only if the data is at the edge too. An edge function near the user that makes three queries in a row to a database in one region pays the long round trip three times; a regional server behind the CDN pays it once. In the Sydney to Virginia example that is roughly 760 ms against 300 ms of TTFB.
So I would cache shared pages at the CDN, use the edge for redirects, auth gates, and picking cached variants, and render near the database. Rendering at the edge makes sense for pages built from replicated data, accepting that it may be a little stale.
Question 4
We switched to SSR and LCP improved, but INP got worse. Why, and what would you do?
SSR moves content earlier but the page still runs the full bundle to hydrate. That is a long task on a mid-range phone, about 1 second per MB of unzipped JavaScript, and taps during it wait.
I would cut what hydrates: keep non-interactive parts as server-only components or static HTML, make the interactive parts islands, and defer the ones below the fold. Streaming with selective hydration also helps the parts the user touches first.
Question 5
How would you pick rendering strategies for a social network?
The signed-in feed is personal, live, and interactive: server-render the first screen, often streamed, then let the client handle scrolling, likes, and new posts. Public post and profile pages are opened from shared links, so they render on the server with Open Graph tags and short shared caching.
Settings and messages are client-rendered behind the login, and marketing pages are static. That is four strategies in one product, each chosen from who sees the page and how it changes.
Practice it
These design problems lean on choosing a rendering strategy.
References
- Rendering on the Web The classic overview of server, static, and client rendering trade-offs.
- Understand JavaScript SEO basics How Googlebot crawls, renders, and indexes JavaScript pages.
- Dynamic rendering as a workaround Why Google no longer recommends serving prerendered HTML only to bots.
- The Open Graph protocol The meta tags link preview bots read from the HTML head.
- Time to First Byte (TTFB) What TTFB includes and how rendering choices move it.
- SvelteKit page options Per-route prerender, ssr, and csr settings, a clear example of mixing strategies.
Read next
- Client-side renderingThe server sends an almost empty HTML page plus a JavaScript bundle, and the browser builds the whole page itself: easy to host and great behind a login, but slow to first content and weak for search.
- Server-side renderingThe server builds the full HTML for each request, so users see content before any JavaScript runs, then the browser hydrates it: faster first paint and better SEO, paid for with server work, a later first byte, and a second render in the browser.
- Static generation and incremental regenerationRender pages to HTML once, ahead of time, and serve the same files from a CDN to everyone; incremental regeneration then refreshes single pages after launch, so you get static speed without rebuilding the whole site for every edit.
- Streaming server renderingThe server sends a page's HTML in pieces as each part becomes ready, instead of waiting for its slowest data: the layout paints right away, slow sections stream in later, and each piece hydrates on its own.
- Islands and partial hydrationShip a page as static HTML and send JavaScript only for the few parts that need it, each waking up on its own trigger, so content pages stay fast on slow phones instead of paying to hydrate the whole app.
- React Server ComponentsReact components that run only on the server and send their rendered result, not their code, to the browser: data fetching moves into components, heavy libraries stay off the client, and only components below a
"use client"boundary ship JavaScript.