Next.js Rendering Strategies: SSG, SSR, ISR, CSR (2026)

Next.js rendering strategies explained for the App Router: SSG, SSR, ISR, CSR and Cache Components, with code for each and a table to pick the right one.

AshFull-stack developer and the maker behind ShadcnDeck. Writes practical guides on React, Next.js, Tailwind, and shadcn/ui — the things he wishes existed when he started.
Published Oct 15, 2026
Updated Oct 15, 2026
16 min read
shadcn/uiReactNext.jsTailwind CSSTypeScriptFrontend DevelopmentApp RouterServer ComponentsRenderingCachingSEO

In the App Router you no longer pick one rendering strategy per page. You decide, component by component, what is static, what is cached, and what runs on every request. Since Next.js 16.4 (October 6, 2026), the Next.js team recommends that model, called Cache Components, for every app.

SSG, SSR, ISR and CSR are still the right vocabulary. This guide maps each one to working code in two models: the previous one (no flag) and Cache Components. You will also get a table for picking a strategy by page type, the SEO details that trip people up, and a list of mistakes with fixes. If you're applying this to a marketing site, our walkthrough for building a landing page with Next.js and shadcn/ui covers the sections themselves; this guide covers how they get rendered.

Last checked against the Next.js 16.4 docs on October 15, 2026. Next.js moves fast, so check the version in your package.json before copying anything.

The short answer

StrategyHTML is producedPrevious modelCache Components
SSGAt build timeStatic by default when nothing reads request data; generateStaticParams for dynamic routes'use cache' with a long cacheLife
ISRAt build time, refreshed laterfetch with next: { revalidate }, revalidateTag, revalidatePath'use cache' + cacheLife + cacheTag
SSROn every requestcookies(), headers(), cache: 'no-store', or dynamic = 'force-dynamic'Uncached data or runtime APIs inside <Suspense>
CSRIn the browserClient Component + SWR, React Query, or use()Same
PPRStatic shell at build time, dynamic parts streamedNot available (experimental.ppr was removed in 16)Default behavior

If a tutorial tells you to use getStaticProps or getServerSideProps, it was written for the Pages Router. Everything below is the App Router.

How Next.js renders a page

You need three terms before the strategies make sense.

  • Server Components are the default for layouts and pages. On the server they are rendered into the React Server Component (RSC) Payload, a compact serialized description of the rendered tree.
  • Client Components (files with 'use client') handle state, event handlers, effects and browser APIs.
  • Hydration is how React attaches event handlers to server-rendered HTML.

On the first load, the browser uses the HTML for a fast, non-interactive preview, the RSC Payload to reconcile the server and client trees, and JavaScript to hydrate Client Components. On later navigations, the RSC Payload is prefetched and cached, and Client Components render entirely in the browser.

Every "strategy" below is an answer to one question: when does this HTML get produced? At build time, at request time, or in the browser.

SSG: static site generation

SSG renders at build time. The result is plain HTML plus an RSC Payload that a CDN can serve without running your server. Use it when the content is the same for every visitor and changes rarely: landing pages, docs, pricing, most blog posts.

Previous model. A page that reads no request data is prerendered. For dynamic routes, tell Next.js which URLs to build with generateStaticParams:

app/blog/[slug]/page.tsx

fetch is not cached by default in the previous model. Setting cache: 'force-cache' makes the intent explicit. A fetch with no cache option is fetched once during next build only if it runs before any request-time API.

Cache Components. Cache the data with 'use cache', give it a lifetime, and tag it so you can refresh it later:

app/blog/[slug]/page.tsx

Three details are specific to Cache Components:

  • generateStaticParams must return at least one param. An empty array is an error, because Next.js needs something to prerender and validate.
  • dynamicParams is not supported. Params you didn't list render on request. If you used dynamicParams = false to reject them, call notFound() when a param doesn't match real data.
  • Whether to await params at the top or inside <Suspense> is a trade-off. Awaiting at the top gives you a real 404 for unknown slugs. Awaiting inside <Suspense> lets Next.js serve the App Shell instantly for unlisted URLs. The ISR with Cache Components guide covers both.

Static export. Setting output: 'export' in next.config.ts builds a fully static site, with one HTML file per route. Next.js server features are not supported there, and 'use cache' isn't supported in a static export either.

ISR: incremental static regeneration

ISR is SSG that refreshes. You serve prerendered content, then regenerate it in the background on a schedule or when something changes. Use it for content that updates but doesn't need to be live: a blog fed by a CMS, product catalogs, changelogs.

Previous model: time-based and on-demand.

tsx
ts

Cache Components: cacheLife and tags. You already saw cacheLife('days') and cacheTag(...) in the blog example. The lifetime does the time-based part. For on-demand refresh, add a webhook your CMS calls when content changes:

app/api/cms-webhook/route.ts

Next.js ships two invalidation functions, and they behave differently:

revalidateTag(tag, 'max')updateTag(tag)
Where it worksServer Actions and Route HandlersServer Actions only
BehaviorServes stale content while fresh content builds in the backgroundExpires the entry immediately
Use it forWebhooks, content where a short delay is fineA user saving something and expecting to see it right away

The Next.js docs recommend tag-based invalidation over revalidatePath when you can, because it's more precise and avoids over-invalidating.

ISR for URLs you didn't prerender. With Cache Components and Partial Prefetching, a first visit to a URL that generateStaticParams didn't list gets the App Shell immediately. Next.js then renders the full page in the background, and the next visitor gets the finished version. That App Shell behavior for unlisted params needs Next.js 16.3 or later.

SSR: server-side rendering

SSR produces HTML on every request. Use it when the output depends on who is asking or on data that must be fresh: a dashboard, a cart, a search results page, anything that reads cookies.

Previous model. Reading request data makes the whole route dynamic:

tsx

You can also force it for a route with export const dynamic = 'force-dynamic', which is equivalent to making every fetch in that route no-store.

Cache Components. Reading cookies() no longer makes the whole route dynamic. Put the request-time part behind <Suspense>, and everything around it stays in the static shell:

tsx

Math.random(), Date.now() and crypto.randomUUID() need the same treatment. They produce a different value on every run, so Cache Components makes you choose. Call connection() from next/server before them and wrap the component in <Suspense> for a value per request. Or cache the result so everyone shares one value until revalidation.

🎨 Reading a theme from a cookie like this? Build the light and dark tokens it switches between in the free shadcn theme generator, then see our fix for the dark-mode flash on server-rendered pages.

CSR: client-side rendering

With CSR, the browser fetches data and builds the UI. Use it for interactive widgets, anything that depends on browser APIs, and screens behind a login where search engines don't matter.

Next.js recommends two ways to fetch in Client Components. The first is a library like SWR or React Query:

tsx

The second is React's use API. Start the request in a Server Component, don't await it, and pass the promise down:

app/dashboard/page.tsx
app/dashboard/stats-chart.tsx

The request starts on the server, so you skip the client-side waterfall of "load JavaScript, then start fetching." The same split works for a sortable shadcn data table: fetch the rows on the server and hand them to the client table as a prop.

The SEO catch. Content that appears only after a client-side fetch is not in the HTML Next.js sends. Crawlers that don't execute JavaScript see the fallback instead. Keep anything that should be indexed outside the client-only boundary.

📊 Turning those stats into a real chart? shadcn charts are Client Components, so the use() pattern above fits them directly. Our shadcn charts guide with copy-paste examples has area, bar and line variants ready to take that data.

Launch your SaaS faster with a modern Shadcn UI template - Explore Free Templates

PPR and Cache Components: the strategy that mixes the others

Partial Prerendering (PPR) renders one route in two layers. At build time Next.js prerenders a static shell: the static content, the cached content, and the <Suspense> fallbacks. At request time it streams in whatever couldn't be prerendered. The shell can be served directly from a CDN.

PPR is now the default behavior with Cache Components. The old experimental.ppr flag and the experimental_ppr route config were removed in Next.js 16.

Here is a pricing page that uses all three kinds of content:

app/pricing/page.tsx

Three rules decide where each component ends up:

  1. 'use cache': the result is cached and joins the static shell, as long as its lifetime isn't too short.
  2. <Suspense>: the fallback joins the static shell and the content streams at request time.
  3. Predictable work such as module imports, pure computation and fs.readFileSync: it completes during prerender and joins the shell.

The Next.js team describes 'use cache' as a component-level version of the Cache-Control header. In practice, you are saying how long a piece of UI or data may be reused.

Turn it on

next.config.ts

New apps from create-next-app have it enabled by default as of Next.js 16.4. Set partialPrefetching explicitly: leaving it unset logs a warning. Cache Components requires the Node.js runtime, so routes that export runtime = 'edge' need to migrate. The Next.js team says it will become the default in Next.js 17.

Already have an app? What changes

When you enable the flag, route segments that still export dynamic, revalidate or fetchCache produce errors. Here is the mapping:

Previous modelWith Cache Components
dynamic = 'force-dynamic'Remove it. Uncached data and request-time APIs already run per request
dynamic = 'force-static'Remove it. Use 'use cache' with cacheLife('max')
revalidate = 3600'use cache' with cacheLife('hours')
fetch(url, { next: { revalidate, tags } })Move the fetch into a 'use cache' function and use cacheLife and cacheTag
unstable_cacheKeeps working. Migrate when convenient
export const dynamicParamsNot supported. Delete it
experimental_pprRemoved. The flag turns PPR on

You don't have to convert every route at once. Setting export const instant = false on a segment marks it as allowed to block, and you convert routes one at a time. The Next.js docs also describe a next-cache-components-adoption skill that walks a coding agent through the migration.

What cacheLife profiles mean

Each profile sets three timings: stale (how long the browser can reuse it without asking the server), revalidate (how often the server refreshes it in the background) and expire (the maximum age before the next request waits for fresh content).

ProfileGood forstalerevalidateexpire
defaultStandard content5 minutes15 minutesnever
secondsReal-time data30 seconds1 second1 minute
minutesFrequently updated content5 minutes1 minute1 hour
hoursSeveral updates a day5 minutes1 hour1 day
daysDaily updates5 minutes1 day1 week
weeksWeekly updates5 minutes1 week30 days
maxRarely changes5 minutes30 days1 year

Short lifetimes change where content can live. A cache with revalidate: 0, an expire under 5 minutes, or the seconds profile is excluded from prerenders and becomes a request-time hole. Wrap those in <Suspense>.

Set a cacheLife in every use cache scope. Without one, the default profile applies silently, and nested caches can shorten an outer cache's lifetime in ways that are hard to trace.

Which strategy for which page

These are our recommendations, based on how the docs describe each tool. Your content and traffic may change the answer.

Page typeApproachWhy
Marketing or landing pageFully static. Consider ensureStatic = 'navigation' (16.4+)One dynamic component can quietly add server cost. This makes the build fail if one sneaks in
Blog or docsPrerender with generateStaticParams. cacheLife('days') plus a cacheTag, refreshed by a CMS webhookContent changes when you publish, not on a timer
Pricing page with a logged-in bannerStatic plans, cached plan data, banner behind <Suspense>Everyone gets the shell instantly; only the banner is per-user
Product or catalog pagePrerender popular items, let the rest render on first visit. Cache product data for hoursAvoids building pages nobody visits
Search resultssearchParams read inside <Suspense>, with the search query cachedThe shell paints at once; results stream
Logged-in dashboardRequest-time data behind <Suspense>, with CSR for live widgetsNo SEO value, so optimize for freshness
Live numbers (scores, stock prices)cacheLife('seconds') behind <Suspense>, or client pollingToo fresh to prerender
Developer portfolioStatic pages. MDX blog posts prerenderedContent changes with a deploy

🧭 Want to see these choices in shipped code? Our list of open-source shadcn/ui projects to study includes dashboards, marketing sites and SaaS apps on the App Router, so you can see where real teams put their client boundaries.

SEO: what each strategy means for crawlers

  • Static shell and prerendered pages put real content in the first HTML response. That helps crawlers and First Contentful Paint.
  • Bots get special handling. Next.js detects HTML-limited bots by user agent and waits for generateMetadata before sending the page. With Cache Components, those bots skip the prerendered shell and get a dynamic render so the metadata lands in <head>. If your shell depends on build-time-only data, a page that loads for people can fail for a crawler. Make sure every data source the shell uses is reachable at request time too.
  • Status codes are locked once streaming starts. If notFound() fires after the first chunk, the response is already 200 OK. Next.js adds <meta name="robots" content="noindex"> instead. Call notFound() before any <Suspense> boundary when you need a real 404, as in the blog example above.
  • Keep your largest above-the-fold element outside <Suspense>. An LCP element inside a boundary can't paint until that boundary's content swaps in. Use the preload prop on next/image for LCP images.
  • Match skeleton sizes to real content. A fallback that is a different height from the final content shifts the layout and hurts CLS.

Common mistakes, and the fix

  1. Awaiting params or cookies() at the top of a layout. Everything below that line becomes request-time and can't join the static shell. Pass the promise down and await it inside a <Suspense> boundary:
    tsx
  2. Reading cookies(), headers() or searchParams inside 'use cache'. It fails. Read the value outside the cached scope and pass it in as an argument. It becomes part of the cache key.
  3. Using Date.now() or Math.random() during prerender. The build errors, and instant = false doesn't clear it. Use connection() plus <Suspense>, or cache the value.
  4. Returning [] from generateStaticParams with Cache Components on. It's an error now. Return at least one real param.
  5. Calling revalidateTag with one argument. The cache profile is a required second argument, for example 'max'. updateTag takes one argument but only works in Server Actions.
  6. Assuming 'use cache' persists on serverless. The default store is in memory. On serverless, entries typically don't survive between requests. Self-hosted servers keep them. 'use cache: remote' uses a durable shared handler. Also, no entry carries over to a new deployment, because the cache key includes the build ID.
  7. Testing ISR in next dev. In the previous model, pages always render on demand in development. Build and start the app (next build && next start) to see real behavior.
  8. Marking big layouts 'use client'. Everything the file imports ships to the browser. Put the directive on the small interactive leaf, like a search box, and keep the layout a Server Component. A site header is the usual case: the components in our shadcn header guide carry 'use client' in the header file, so app/layout.tsx stays on the server.

How to check what Next.js actually did

  • Read the dev overlay. With Cache Components on, Next.js validates routes in development and shows insights such as blocking-route, with suggested fixes. They don't appear in the HTTP response, which still returns 200.
  • Turn on cache logging. NEXT_PRIVATE_DEBUG_CACHE=1 npm run start prints verbose cache activity, including ISR.
  • View the raw HTML. Use view-source: or curl to confirm your content is in the first response.
  • Watch the stream. In Chrome DevTools, a long "Content Download" phase with an early Time to First Byte means the response is streaming. To see individual chunks, run a small script against a production build:
    stream-check.mjs
    Add 'User-Agent': 'Twitterbot/1.0' to the headers to see how a bot is served. Reverse proxies, CDNs and compression can buffer a stream, so test through the same path your users take.

Coming from the Pages Router

Pages RouterApp Router with Cache Components
getStaticProps with revalidate'use cache' with cacheLife
getStaticPathsgenerateStaticParams
fallback: truePartial Prefetching on, params awaited inside <Suspense>
fallback: 'blocking'Partial Prefetching off, params awaited outside <Suspense>
getServerSidePropsRequest-time data inside <Suspense>

Try it on a real codebase

Landing pages are the easiest place to practice static rendering, because most of the page doesn't depend on the visitor. If you want a Next.js 16 codebase to read while you apply this, ChatDeck is our free, MIT-licensed SaaS landing page template (Next.js 16, React 19, Tailwind CSS v4, shadcn/ui).

If you outgrow one page and need a marketing site with a blog, FoxCopy is our paid template with a working blog. A blog is the classic route for the tag-and-revalidate pattern above.

We make both templates, so weigh that accordingly. ShadcnDeck isn't affiliated with Vercel or the Next.js team. For a step-by-step build, see how to build a landing page with Next.js and shadcn/ui, and for more starting points, our roundup of free shadcn templates.

Sources

Launch your SaaS faster with a modern Shadcn UI template - Explore Free Templates

Frequently Asked Questions

A
Ash

Full-stack developer and the maker behind ShadcnDeck. Writes practical guides on React, Next.js, Tailwind, and shadcn/ui — the things he wishes existed when he started.

Related Articles