logo

Imad Attif

Next.js Rendering Explained: CSR, SSR, SSG, ISR, and PPR

Imad Attif, Sr. Frontend Engineer

16 min read

Aug 1, 2026

Next.js rendering comes with a pile of acronyms: CSR, SSR, SSG, ISR, RSC, PPR. Each one answers the same two questions differently: where does your HTML get generated (browser or server), and when (at build time, at request time, or somewhere in between)?

This post explains every strategy in order, with code, trade-offs, and the situations each one fits. It ends with Partial Prerendering (PPR), which matters most today: as of Next.js 16 with Cache Components, PPR is the default rendering behavior, and it quietly absorbs all the older modes. Understanding the older strategies is still worth it, both because you'll encounter them in existing codebases and because PPR only makes sense as the answer to their trade-offs.

The one-line summary of each:

  • CSR (client-side rendering): the browser downloads JavaScript and builds the page itself. Fast to deploy, slow first paint, weak SEO.
  • SSR (server-side rendering): the server builds fresh HTML for every request. Always current, but every visitor waits for the server to work.
  • SSG (static site generation): HTML is built once at deploy time. Instant to serve, but stale until the next deploy.
  • ISR (incremental static regeneration): static pages that rebuild themselves in the background on a timer or on demand.
  • Streaming: send the ready parts of the page now, let the slow parts arrive as they finish.
  • PPR (partial prerendering): one page, both worlds. A static shell serves instantly while dynamic holes stream in per request.

What problem do rendering strategies solve?

A page is HTML, and something has to produce it. Producing it involves work: fetching data, running components, assembling markup. The entire rendering discussion is about scheduling that work:

  • Do it in the browser, and your server stays simple but users stare at a blank page while JavaScript loads and runs.
  • Do it on every request, and content is always fresh but every user pays the rendering cost, and your server pays it under load.
  • Do it once at build time, and serving is nearly free but the content freezes until you rebuild.

Fresh, fast, cheap: for most of the web's history you could pick two. Each strategy below is a different pick, and PPR is the framework's attempt to stop making you choose per page.

What is client-side rendering (CSR)?

With CSR, the server sends a nearly empty HTML file plus a JavaScript bundle. The browser downloads the bundle, runs it, fetches data, and builds the page in place. This is how classic single-page apps (Create React App style) work.

In modern Next.js, you get CSR for a piece of UI with a Client Component that fetches its own data:

Strengths: perfect for UI that's interactive, personal, or constantly changing after load (dashboards behind a login, live widgets, editors). No server rendering cost at all.

Weaknesses: the first paint waits on the full JavaScript-download-parse-execute-fetch chain, which is brutal on slow devices and networks. Content that only exists after JavaScript runs is also second-class for SEO and link previews.

Use it for the interactive islands inside a page, not for the page itself. In Next.js you almost never choose CSR for whole routes anymore; you choose it per component with 'use client'.

What is server-side rendering (SSR)?

With SSR, the server runs your components on every request and sends finished HTML. The user sees content immediately, then JavaScript hydrates the page to make it interactive.

In the Pages Router era, this was getServerSideProps. In the App Router, a route renders per request whenever it reads request-time data: cookies(), headers(), searchParams, or any uncached fetch.

Strengths: the response always reflects this moment and this user. Full HTML for crawlers and previews.

Weaknesses: every request pays the full rendering cost, including data fetching, before the user sees anything. Under traffic, your server (and database) does that work over and over for pages that may be identical. Time-to-first-byte depends on your slowest query.

Use it for genuinely per-request pages: authenticated dashboards, pages that depend on cookies or geolocation, search results.

What is static site generation (SSG)?

With SSG, pages are rendered once, at build time. The output is plain HTML files that a CDN can serve instantly to anyone, anywhere. There is no per-request rendering work at all.

In the App Router, a route with no request-time data and no uncached fetches is prerendered automatically. For dynamic routes like /posts/[slug], you tell Next.js which pages to build with generateStaticParams:

app/posts/[slug]/page.tsx

At build time, Next.js renders every slug into HTML. Requests never touch your data source.

Strengths: the fastest possible serving (it's a file on a CDN), effectively infinite scalability, great SEO, and near-zero server cost.

Weaknesses: content is frozen at build time. A typo fix means a redeploy. A site with 50,000 pages means a long build. Nothing can be personalized.

Use it for content that changes rarely and looks the same for everyone: marketing pages, docs, blogs.

What is incremental static regeneration (ISR)?

ISR keeps SSG's serving speed while fixing the staleness. Pages are static, but the server regenerates them in the background after a revalidation window, or immediately when you tell it to. Visitors always get the cached copy instantly; the refresh happens behind the scenes (stale-while-revalidate).

The classic App Router form is a revalidate export or a fetch option:

In the Cache Components era, the same idea is expressed with use cache plus cacheLife, and on-demand invalidation with cacheTag and updateTag or revalidateTag. Same semantics, moved from route-level config into the code it affects. (I cover that model in depth in the use cache post.)

Strengths: static speed with content that heals itself. On-demand revalidation (a CMS webhook firing revalidateTag) gets publish-to-live down to seconds. Huge sites can skip prerendering rare pages and generate them on first visit.

Weaknesses: the first visitor after a change can still see the old version (that's the "stale" in stale-while-revalidate), and it remains per-page: one page is still either static or not.

Use it for content sites with a CMS, e-commerce catalogs, anything where "fresh within a minute" is fresh enough.

What is streaming?

The strategies so far treat a page as all-or-nothing: nothing is sent until everything is rendered. Streaming breaks that. The server sends HTML in chunks as parts of the page finish, so one slow data source stops blocking everything else.

In Next.js, <Suspense> marks the seams:

The header and the skeleton arrive first; when the slow Reviews query resolves, its HTML streams down and replaces the skeleton in place, no client-side refetch involved. A loading.tsx file does the same thing at the route level.

Streaming isn't a separate strategy so much as an upgrade to server rendering, and it's the mechanism PPR is built on.

A note on Server Components

React Server Components (RSC) are often lumped in with SSR, but they're a different axis. Server Components are about what ships to the browser: they run only on the server, and their JavaScript never goes to the client, shrinking bundles. SSR/SSG/ISR are about when HTML is produced. A Server Component can be rendered statically at build time or dynamically per request. The acronyms compose; they don't compete.

What is partial prerendering (PPR)?

Here's the tension all of the above leaves you with: rendering has always been a per-page decision, but real pages are mixed. A product page is 90% the same for everyone (title, images, description) and 10% personal (cart, recommendations). Under the classic model, that one cookie read makes the entire page SSR, and you give up static speed for everything else on it.

PPR ends the per-page decision. Each route gets:

  1. A static shell, prerendered and served instantly from the edge: everything that doesn't depend on the request, including anything cached with use cache, plus the fallbacks of every <Suspense> boundary.
  2. Dynamic holes that render per request and stream into the shell: everything that reads cookies, headers, params, or uncached data.

One HTTP response, static start, dynamic finish. With cacheComponents: true in Next.js 16, this is simply how every route renders. A full page looks like this:

The product details ship in the prerendered shell, so first paint is as fast as pure SSG. The cart streams in per request, so it's as fresh as pure SSR. Neither compromises the other, and each piece of the page states its own rendering behavior in its own code.

Notice what happened to the old acronyms:

  • A page whose components are all cached or static is effectively SSG.
  • Cached components with a cacheLife are ISR.
  • A page that's all dynamic holes behaves like SSR (streamed).
  • Client Components inside any of it are CSR islands.

They're no longer modes you pick per page. They're descriptions of what different parts of one page do.

One more thing PPR changes: it's strict. If a component reads uncached data without being wrapped in <Suspense> or cached with use cache, Next.js raises an error in development and at build time instead of silently making the whole route dynamic. The old implicit "one cookie read de-optimizes the page" behavior is now an explicit choice you're asked to make: stream it, cache it, or deliberately allow the route to block.

How do you choose a rendering strategy?

With Cache Components enabled, the question shifts from "which mode does this page use?" to "what does each part of this page need?" Component by component:

  • Same for every user, changes rarely? Cache it with use cache and a long cacheLife (or leave it static). It joins the shell.
  • Same for every user, changes on a schedule or via CMS? Cache it, tag it, revalidate on demand. That's your ISR.
  • Depends on the request or user? Leave it uncached behind a <Suspense> boundary. It streams.
  • Interactive after load, or updates live in the browser? Client Component. That's your CSR island.
  • Working in an older codebase without Cache Components? The per-page rules still apply: generateStaticParams and no dynamic APIs for SSG, revalidate for ISR, dynamic APIs or getServerSideProps for SSR.

If you take one habit from this post: stop asking "should this page be static or dynamic?" and start asking "which parts of this page are static, and which aren't?"

FAQ

What's the difference between SSR and SSG? Timing. SSR renders HTML on the server for every request, so it's always fresh but every visitor waits for the render. SSG renders once at build time, so serving is instant but content is frozen between deploys. ISR sits between them: static serving with automatic background regeneration.

Is SSR bad for performance? Not inherently; it's a trade. You pay rendering time on every request in exchange for freshness and personalization. It becomes a problem when applied to pages that didn't need it, which was easy to do accidentally in the old model where one cookies() call made an entire route dynamic. PPR fixes exactly that.

Is PPR stable? Yes. Partial Prerendering is the default rendering behavior when Cache Components is enabled (cacheComponents: true) in Next.js 16. The old experimental.ppr flag is gone, folded into that single option.

Do getStaticProps and getServerSideProps still exist? Only in the Pages Router, which is still supported but no longer where new features land. The App Router replaced them with the model described here: static by default, dynamic via request APIs, cached via use cache, with generateStaticParams covering the getStaticPaths role.

Are React Server Components the same as SSR? No. Server Components control what JavaScript ships to the browser (theirs doesn't). SSR controls when HTML is generated. A Server Component can be part of a static, ISR, or per-request render. The two are independent and combine freely.

Why not just use SSG with a client component instead of PPR? Both serve a static shell instantly, but the dynamic part behaves very differently. With a client component, the data fetch can't start until the browser downloads the bundle, hydrates, and runs the component, so you pay two sequential round trips, need an API route for the data, ship the fetching code to the client, and crawlers never see the result. With PPR, the dynamic hole is a Server Component: rendering starts the moment the request arrives, in parallel with the shell, it reads cookies or the database directly with no API route, none of its JavaScript ships to the browser, and the streamed HTML is visible to crawlers. Client islands still win for content that keeps updating after load (live tickers, websocket feeds), and the two compose: a PPR page can contain client islands inside its streamed holes.

Which strategy is best for SEO? Anything that puts real HTML in the response: SSG, ISR, SSR, and PPR's static shell all work. Pure CSR is the weak one, since content only exists after JavaScript runs. With PPR, put your SEO-critical content in cached or static components so it lands in the shell rather than streaming in later.

Summary

  • Every rendering strategy is an answer to where and when HTML gets made: browser (CSR), server per request (SSR), build time (SSG), build time with self-healing (ISR).
  • Streaming with Suspense removed the all-or-nothing constraint: send what's ready, stream what isn't.
  • PPR makes the choice per component instead of per page: a prerendered static shell for the stable parts, streamed dynamic holes for the rest, in a single response.
  • In Next.js 16 with Cache Components, PPR is the default. use cache decides what joins the shell, <Suspense> marks what streams, and the framework errors loudly instead of silently de-optimizing.
  • The old acronyms didn't die; they became local. SSG, ISR, SSR, and CSR now describe parts of a page rather than whole pages.

The acronym soup was never really about six competing technologies. It was one question, "when should this work happen?", asked at coarser and coarser grain until PPR finally let you answer it where it belongs: in each component.