← All writing

Where Should You Render? A Field Guide to Web Rendering Strategies

CSR, SSR, SSG, ISR, and the edge — what each one actually optimizes for, the metrics that separate them, and a decision rule you can apply per route.

"Should this page be server-rendered?" is the wrong question, because it assumes one answer for a whole app. The right unit is the route. Each route has its own data freshness needs, its own SEO stakes, and its own tolerance for a blank first paint — and modern frameworks let you pick a strategy per route. This is a map of the options and when each wins.

The five strategies

  • CSR (client-side rendering): ship an empty shell, fetch and render in the browser.
  • SSR (server-side rendering): render HTML per request on the server.
  • SSG (static site generation): render HTML once at build time.
  • ISR (incremental static regeneration): SSG, but pages re-generate in the background after a TTL.
  • Edge: SSR/ISR executed close to the user at a CDN node.

The metric that decides it

Most of the debate collapses into one number: time to first meaningful paint, and how it trades against data freshness. A useful mental model is that total perceived latency is roughly

Tpaint=Tnetwork+Tserver+ThydrateT_{\text{paint}} = T_{\text{network}} + T_{\text{server}} + T_{\text{hydrate}}

CSR pushes Tserver0T_{\text{server}} \to 0 but pays a large ThydrateT_{\text{hydrate}} and a blank screen while data loads. SSG pushes both TserverT_{\text{server}} and TnetworkT_{\text{network}} toward their floor because the HTML is already sitting on a CDN — at the cost of freshness. The whole art is choosing which term you're willing to inflate.

Illustrative trade-off: SSG wins on time-to-first-byte but risks serving stale content; SSR is always fresh but pays a server cost per request.

Picking per route

Here's the rule I actually use when wiring up a Next.js app:

app/blog/[slug]/page.jsx
// Static by default — the content changes only when I redeploy.
export const dynamic = 'force-static'
 
export function generateStaticParams() {
  return getAllSlugs().map((slug) => ({ slug }))
}
 
// A dashboard route, by contrast, opts into per-request rendering:
//   export const dynamic = 'force-dynamic'

The heuristic in one table:

Route typeFreshness needBest fit
Marketing / docsLowSSG
Blog / articlesLow–mediumSSG or ISR
Product listingsMediumISR
Personalized dashboardPer-requestSSR / Edge
Highly interactive appClient stateCSR shell

A concrete example

This very blog is SSG. Every post is an MDX file compiled at build time into static HTML and dropped onto a CDN — so $T_{\text{server}} = 0$ at request time. The trade-off (content only updates on redeploy) is exactly right for writing: articles don't change between deploys, and I'd rather pay the cost once at build than on every read.1

If these were product pages with hourly price changes, I'd switch the same route to ISR with a short revalidation window and get 90% of the speed with a fraction of the staleness.2

The takeaway

Don't pick a rendering strategy for your app. Pick one per route, driven by how fresh the data must be and how much your users pay for a blank first paint. Frameworks made this a per-route decision on purpose — use that.

Footnotes

  1. Osmani, A. (2019). Rendering on the Web. web.dev — a still-excellent taxonomy of these trade-offs.

  2. Next.js documentation, Incremental Static Regeneration, nextjs.org/docs.

© 2026 · Vibhav Yadav · Built with Next.js