Skip to main content

Jun 2026 · Guide · ~10 min read

SSR vs SSG vs ISR in Next.js — Plain English Explanation

By Safdar Ali — frontend engineer, Bengaluru

I'm Safdar Ali. For six months I treated every Next.js page like SSR because "server" sounded safest. Marketing pages were slower than they needed to be; dashboard pages were over-cached. SSR vs SSG in Next.js is not a popularity contest — it is a delivery choice: when HTML is built, how often it refreshes, and who pays the compute bill. This article is the plain-English map I wish I had, with code for all three modes and a flowchart you can screenshot.

Three acronyms, one question: when is HTML built?

SSR (Server-Side Rendering) builds HTML on each request (or per-request cache miss). Fresh data, higher server load.

SSG (Static Site Generation) builds HTML at deploy time. Fastest CDN delivery; stale until you redeploy unless you add revalidation.

ISR (Incremental Static Regeneration) is SSG plus background refresh — static speed with a TTL. In App Router this is mostly fetch with next.revalidate.

// Mental model — same page, three delivery timings

// SSG:  HTML built at deploy ──────────────────► CDN serves file
// ISR:  HTML built at deploy ──► stale ──► regen in background after N sec
// SSR:  HTML built when user hits URL ─────────► server sends fresh HTML

None of these replace React. They describe when your React tree becomes HTML the browser can paint before hydration.

Decision flowchart — which mode to pick

                    START: New Next.js page
                              |
                              v
                    Does Google need HTML
                    with real content on
                    first request?
                         /        \
                       NO          YES
                       |            |
                       v            v
              Authenticated      Is data identical
              dashboard / SPA?   for all users?
                    |                 /      \
                   YES               NO      YES
                    |                |        |
                    v                v        v
            "use client" +       SSR or      Changes
            client fetch         per-user    every hour+
            (no SSR win)         data?          /    \
                                /    \        NO    YES
                              YES    NO        |      |
                               |      |        v      v
                               v      v       SSG    ISR
                              SSR   ISR      (rare) (revalidate)
                                    or SSG
                                    + short
                                    revalidate

Print that flowchart. When you are unsure, default marketing to ISR with a sensible revalidate, default logged-in dashboards to client rendering.

SSG — build once, serve from the edge

Use SSG for pages where data changes only when you deploy — legal pages, about pages, rarely updated landing heroes.

// app/about/page.tsx — static by default (no fetch cache opts)
export default function AboutPage() {
  return (
    <main>
      <h1>About Safdar Ali</h1>
      <p>Frontend engineer, Bengaluru — available for React / Next.js work.</p>
    </main>
  );
}

// Dynamic routes can still be SSG if you export generateStaticParams
export async function generateStaticParams() {
  const slugs = await getAllBlogSlugs();
  return slugs.map((slug) => ({ slug }));
}

Tradeoff: a price change on an e-commerce SKU list will not appear until redeploy or ISR kicks in. For truly static content, that is a feature — zero origin load.

ISR — static speed, controlled freshness

// app/products/page.tsx — ISR via fetch revalidate (App Router)
type Product = { id: string; name: string; price: number };

async function getProducts(): Promise<Product[]> {
  const res = await fetch("https://api.example.com/products", {
    next: { revalidate: 3600 }, // ISR: refresh at most every hour
  });
  if (!res.ok) throw new Error("Failed to load products");
  return res.json();
}

export default async function ProductsPage() {
  const products = await getProducts();
  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>{p.name} — ₹{p.price}</li>
      ))}
    </ul>
  );
}

First visitor after TTL might still see stale HTML while regeneration runs — acceptable for catalog pages, unacceptable for live stock tickers. Know your freshness requirement before picking ISR.

SSR — per-request HTML when data must be fresh

// app/dashboard/summary/page.tsx — SSR: no cache on fetch
async function getSummary(userId: string) {
  const res = await fetch("https://api.example.com/summary/" + userId, {
    cache: "no-store", // SSR — always fresh for this user
  });
  return res.json();
}

export default async function SummaryPage() {
  const session = await getSession();
  const summary = await getSummary(session.userId);
  return <SummaryClient data={summary} />;
}

SSR costs more at scale. Use it when personalisation or real-time correctness beats CDN economics — not because "server sounds professional."

SSR vs SSG vs ISR — comparison table

CriteriaSSGISRSSR
When HTML is builtDeploy timeDeploy + periodic regenEach request
TTFB on CDNFastestFast (often cached)Slower
Data freshnessStale until deployTTL-basedReal-time
Server costLowestLow–mediumHighest
SEOExcellentExcellentExcellent
Per-user dataPoor fitPoor fitGood fit
App Router signalDefault static pagerevalidate: Ncache: no-store

Marketing site vs dashboard — what I actually ship

Marketing site (public, SEO, shared content): ISR for blog and product listings, SSG for about/legal, Server Components for HTML. On a client rebuild I moved the blog to revalidate: 86400 and saw origin requests drop 70% — part of the story in my Next.js performance case study.

Dashboard (authenticated, personal): client components, SWR or WebSockets, no ISR fantasy. Trying to ISR user-specific charts is how teams waste a sprint.

// BEFORE — marketing page forced to SSR "just because"
export default async function PricingPage() {
  const plans = await fetch("https://api.example.com/plans", { cache: "no-store" });
  // Every visitor hits origin — same JSON for everyone
}

// AFTER — ISR: shared plans, CDN-friendly
export default async function PricingPage() {
  const res = await fetch("https://api.example.com/plans", { next: { revalidate: 3600 } });
  const plans = await res.json();
  // ...
}

My production setup

In production I label routes in file comments — ISR 1h or SSR session — so the next developer does not "optimise" a dashboard into ISR by accident. Pair this with RSC vs client components — rendering mode and component boundary are two separate decisions.

At my day job, the mistake I see most is SSR everywhere because the team learned Pages Router getServerSideProps first. App Router caching is more granular — use it.

The single takeaway

Pick rendering by freshness and audience, not acronym prestige. Marketing: ISR + SSG. Dashboards: client. Personalised SSR only when you mean it.

Related: App Router complete beginner guide. Contact.

If this helped you

I publish free tutorials and write-ups like this in my spare time — no paywall on the guides. If it saved you an afternoon of trial and error, you can support the work:

More guides on safdarali.in — same author, production-focused.

"Talk is cheap. Show me the code."