Next.js

Next.js SSR vs SSG vs ISR Explained

Server-side rendering, static generation, and incremental static regeneration in Next.js — what each is for and how to choose.

Tayyab Roy7 min read
Share:

Every rendering strategy in Next.js answers the same question differently: when does the HTML for this page get generated? Static generation answers “at build time.” Server-side rendering answers “on every request.” Incremental static regeneration answers “at build time, then periodically after that, in the background.” Picking the wrong one for a given route is one of the most common sources of both performance problems and confusing staleness bugs in production Next.js apps.

In the Pages Router, this was an explicit choice between getStaticProps, getServerSideProps, and getStaticProps with revalidate. In the App Router, it’s driven by how you fetch data and a handful of route segment config options. The underlying concepts are identical — only the API surface changed.

Static Site Generation (SSG)

With SSG, the page is rendered to HTML once, at build time, and served as a static file from then on (or from a CDN edge cache). There’s no per-request rendering cost, which makes it the fastest and cheapest option by a wide margin.

// app/blog/[slug]/page.tsx
export const dynamic = 'force-static';

export async function generateStaticParams() {
  const posts = await getAllPostSlugs();
  return posts.map((slug) => ({ slug }));
}

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return <article dangerouslySetInnerHTML={{ __html: post.html }} />;
}

By default, a fetch() call inside a Server Component is cached indefinitely unless you tell it otherwise, which is what makes a route eligible for static generation in the first place:

// Cached indefinitely — contributes to a static route
const res = await fetch('https://api.example.com/posts', { cache: 'force-cache' });

Use SSG for: marketing pages, documentation, blog content, pricing pages — anything where the content is the same for every visitor and doesn’t change on every deploy. It’s the right default for the majority of a typical content site.

Don’t use SSG for: pages where content is personalized per user (a dashboard, a cart), or where staleness of more than a few minutes is unacceptable (a live leaderboard, stock prices).

Server-Side Rendering (SSR)

With SSR, the page is rendered to HTML on the server for every incoming request. This guarantees the content is always current, at the cost of a render on every hit — no caching shortcut, so your server (or serverless function) does real work on every request.

// app/dashboard/page.tsx
export const dynamic = 'force-dynamic';

export default async function Dashboard() {
  const session = await getSession();
  const orders = await fetch(`https://api.example.com/orders?user=${session.userId}`, {
    cache: 'no-store',
  }).then((r) => r.json());

  return <OrdersList orders={orders} />;
}

cache: 'no-store' on a fetch() call (or reading cookies/headers, or using export const dynamic = 'force-dynamic') opts the whole route into dynamic, per-request rendering. This is the same mechanism — it’s not a separate API from SSG, just the opposite caching choice.

Use SSR for: authenticated, personalized pages where every user sees different data, or pages where the underlying data changes so frequently that even a short cache window would show stale information (real-time pricing, inventory counts tied directly to a live system).

Don’t use SSR for: anything that could tolerate even a minute of staleness — you’re paying a full render cost on every request for no real benefit if the data barely changes.

Incremental Static Regeneration (ISR)

ISR is the middle ground: the page is generated statically, served from cache like SSG, but after a configured time window, the next request triggers a background regeneration. Visitors never wait for that regeneration — they get the (slightly stale) cached version while Next.js rebuilds the page behind the scenes, then swaps in the fresh version for subsequent requests.

// app/products/[id]/page.tsx
export const revalidate = 300; // seconds

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await fetch(`https://api.example.com/products/${params.id}`, {
    next: { revalidate: 300 },
  }).then((r) => r.json());

  return <ProductDetails product={product} />;
}

You can also trigger on-demand revalidation from a webhook or server action instead of (or in addition to) a time-based window, which is the pattern I reach for most often in e-commerce and CMS-backed sites:

// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const { path, secret } = await request.json();

  if (secret !== process.env.REVALIDATE_SECRET) {
    return NextResponse.json({ message: 'Invalid secret' }, { status: 401 });
  }

  revalidatePath(path);
  return NextResponse.json({ revalidated: true });
}

That lets a CMS or inventory system push a signal the instant content actually changes, rather than waiting out a fixed time window or re-rendering on every single request.

Use ISR for: product catalogs, listing pages backed by a CMS, anything that changes periodically but not on every request — ISR gives you near-static performance with a bounded staleness window you control.

Don’t use ISR for: per-user personalized content — the regenerated page is shared across all visitors to that URL, so anything user-specific will leak between sessions if you try to force it through ISR.

Choosing Between Them

Strategy Rendered Best for Staleness
SSG Build time Marketing, docs, blogs None until next deploy
ISR Build time + background refresh Product catalogs, CMS content Bounded (your revalidate window)
SSR Every request Dashboards, personalized data None, but costs a render every time

A practical way to decide: start by asking whether the page is the same for every visitor. If not, it’s SSR. If it is, ask how often the underlying data changes. If “never until the next deploy,” SSG. If “periodically, and a few minutes of staleness is fine,” ISR.

It’s also worth remembering these aren’t mutually exclusive within a single app — a typical e-commerce site statically generates its blog and marketing pages, uses ISR for product and category pages, and uses SSR only for the cart and account dashboard. Mixing strategies per-route is the normal, correct approach, not a compromise.

This decision also interacts directly with how you structure Server and Client Components — see Next.js Server Components vs Client Components for how the two models fit together. And because rendering strategy has a direct effect on your Largest Contentful Paint and overall load performance, it’s worth reading alongside Next.js Image Optimization and Performance and the broader Frontend Performance Optimization Techniques.

Common Mistakes

Defaulting every route to SSR “to be safe.” This throws away free performance. If the page doesn’t actually need per-request data, you’re paying a render cost for nothing.

Setting an ISR revalidate window so short it behaves like SSR. A revalidate: 1 is effectively asking for a background regeneration on almost every request, which defeats the point — you’re carrying the complexity of ISR without the caching benefit.

Forgetting that cookies() or headers() opt a route into dynamic rendering. Even if you intended a route to be static, calling either of these inside a Server Component forces it dynamic, which can silently turn an entire route SSR when you expected ISR or SSG.

FAQ

Can I mix SSR and ISR within the same page? Not within a single route segment — a route is either dynamic or it isn’t. But you can compose a page from both by fetching some data with cache: 'no-store' (dynamic) and wrapping the dynamic part in a <Suspense> boundary while the rest of the page stays static, using partial prerendering patterns.

Does ISR work on every hosting platform? It depends on the platform’s support for Next.js’s incremental cache. Vercel supports it natively; self-hosting requires a persistent filesystem or a custom cache handler, since the regenerated pages need to be stored somewhere between requests.

Is SSG still relevant with ISR available? Yes — SSG is just ISR with no revalidate window (or a route explicitly marked force-static). If content truly never changes without a new deploy, there’s no reason to pay even the minimal overhead of a revalidation check.

#Next.js#Rendering#SSR#SSG#ISR

Written by

Tayyab Roy

Senior Frontend / Full-Stack Engineer

Senior Frontend & Full-Stack Engineer specializing in Angular, React, Next.js and enterprise eCommerce (Shopify, SAP Commerce / Hybris, Spartacus). I write about practical, production-tested frontend engineering.