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.
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 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 saycache: '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
generateStaticParamswith 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_cachefor expensive computations that are not driven byfetch. - Measure with logs. A page that says “static” in
next buildbut 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.
Related articles
- Next.js Next.js Static Export vs Server: Picking the Right Output
Compare Next.js static export and server runtime modes. Understand when each shines, how features differ, and how to choose without painting yourself into a corner.
- React React Server Components Explained
A practical guide to React Server Components: how they differ from client components, the rendering model, streaming, and when to reach for each.
- Next.js Error Boundaries in the Next.js App Router
How error.tsx, global-error.tsx, and not-found.tsx work in the Next.js App Router — when each one fires, how segments isolate failures, and patterns for recovery and observability.
- Next.js Next.js Caching Strategies Explained
Walk through the four caching layers in Next.js App Router and learn how to choose static, ISR, dynamic, or per-request fetch caching.