Skip to content
C Codeloom
Next.js

Next.js ISR vs SSR vs SSG

A clear comparison of static, server-rendered, and incrementally regenerated pages in Next.js, with rules for choosing the right strategy.

·5 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • What SSG, SSR, and ISR really do at request time
  • How the App Router maps these to fetch options
  • When each strategy makes sense
  • How revalidation works in practice
  • Pitfalls around personalized content

Prerequisites

  • Basic Next.js

Next.js used to make you pick between getStaticProps and getServerSideProps. The App Router replaced that with fetch options and dynamic flags, which is more flexible but also more confusing. Three strategies underlie everything: static generation (SSG), server-side rendering (SSR), and incremental static regeneration (ISR). Knowing when each one wins keeps your pages fast and your bills low.

The three strategies in one paragraph

SSG builds HTML at build time, ships it from a CDN, and never touches your server on a request. SSR builds HTML per request, on the server, with fresh data. ISR builds at request time too, but caches the result and only rebuilds on a schedule or on demand. ISR is the compromise: mostly static performance, with some freshness.

Mental model

SSG:  build time -> HTML in CDN -> served forever (until next build)

SSR:  request -> server renders -> HTML to client (every time)

ISR:  request -> serve cached HTML
              -> stale? rebuild in background -> update cache
Where rendering happens for each strategy

The axis to think about is “how often does the data change vs how many users see it?” High traffic + slow-changing content is the sweet spot for SSG and ISR. Per-user or per-second-fresh content is SSR.

App Router mapping

In the App Router, the rendering strategy is mostly inferred from how you fetch data and whether you use dynamic APIs.

// Static (SSG): no dynamic APIs, no opt-outs
export default async function Page() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());
  return <PostList posts={posts} />;
}
// ISR: revalidate every 60 seconds
export default async function Page() {
  const posts = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 },
  }).then(r => r.json());
  return <PostList posts={posts} />;
}
// SSR: opt out of caching
export const dynamic = 'force-dynamic';

export default async function Page() {
  const posts = await fetch('https://api.example.com/posts', { cache: 'no-store' }).then(r => r.json());
  return <PostList posts={posts} />;
}

Using cookies(), headers(), or searchParams makes the route dynamic by default; you cannot statically generate a page that depends on the current user.

ISR by tag, the underrated feature

revalidateTag lets your backend invalidate cached pages on demand. This turns ISR from a clock-based cache into an event-driven one.

const posts = await fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] },
}).then(r => r.json());
// app/api/webhooks/cms/route.ts
import { revalidateTag } from 'next/cache';

export async function POST() {
  revalidateTag('posts');
  return Response.json({ ok: true });
}

A CMS webhook hits the endpoint, the cache invalidates, the next visitor triggers a fresh render. Visitors during the rebuild see the previous version until the new one is ready, so no spike of slow requests.

How to choose

Use SSG when: content changes only at deploy, traffic is high, latency matters. Marketing pages, docs, blogs.

Use ISR when: content changes between deploys, but not every second. Product pages, listings, public profiles. Combine with tags for instant updates.

Use SSR when: content depends on the user or must be exact. Dashboards, carts, anything authenticated. Also when SEO needs exact freshness (rare).

Common pitfalls

  • Calling cookies() in a layout, which forces every page under it into SSR. Move the call into a leaf component.
  • Setting revalidate: 0. That is SSR; just say cache: 'no-store' for clarity.
  • Mixing static and dynamic fetches in one route and expecting the page to stay static. The most dynamic fetch wins.
  • Forgetting that ISR revalidation runs on a request. With zero traffic, the page never refreshes.
  • Hoping a CDN handles personalization. The CDN serves the same HTML to everyone; do not put per-user data in an SSG page.
  • Building 100k SSG pages and watching builds time out. Use generateStaticParams with a subset and let ISR fill the rest.

Practical tips

  • Default to ISR. It is the best price/freshness ratio for most public pages.
  • Tag everything you might want to invalidate. Adding a tag later means a deploy; adding it now is free.
  • Keep dynamic data in client components fetched via Route Handlers when SSR would push too much per request.
  • Use unstable_cache for expensive computations that are not driven by fetch.
  • Measure with logs. A page that says “static” in next build but logs on every request is silently SSR.

Wrap-up

SSG is cheap and fast but stale. SSR is fresh but expensive. ISR is the pragmatic middle, and with on-demand revalidation it usually wins. Pick per page, not per app, and let the data lead the choice.