# ISR vs SSG vs SSR in Next.js 15: How to Choose

> In Next.js 15, SSG renders a page once at build time, ISR serves that static page and regenerates it in the background after a revalidate interval or an on-demand call, and SSR renders on every request. Choose SSG for content that changes only on deploy, ISR for content that changes without a deploy, and SSR only when the page depends on the request itself.

- Author: Sami Ullah (https://samiullah.tech/about)
- Published: 2026-11-11
- Updated: 2026-11-13
- Tags: nextjs, performance, seo
- Canonical: https://samiullah.tech/blog/isr-vs-ssg-vs-ssr-nextjs

Every Next.js route makes a choice about when its HTML is built: once at build time, again later in the background, or on every request. That choice decides your page speed, your hosting bill and how fresh your content is. This post explains ISR vs SSG vs SSR in Next.js 15 App Router terms, shows how to force each one, and ends with the checklist I use to pick.

Examples target Next.js 15 and were checked in November 2026. The App Router does not use the old `getStaticProps` and `getServerSideProps` names, so I map the concepts to the new APIs as I go.

## What are SSG, ISR and SSR?

SSG, ISR and SSR are three answers to the question "when is this page's HTML generated?": at build time, at build time plus periodic regeneration, or on every request.

- **SSG (Static Site Generation)** renders the page once during `next build`. The HTML is stored and served as a file until the next deploy.
- **ISR (Incremental Static Regeneration)** starts like SSG, but the page can be regenerated in the background after a time window or when you call `revalidatePath` or `revalidateTag`. Visitors always get a cached page, then the next visitor gets the new one.
- **SSR (Server-Side Rendering)**, which Next.js now calls dynamic rendering, builds the HTML on the server for each request.

All three send full HTML to the browser. The difference is when the server does the work and how stale the result can be.

## How does Next.js 15 decide which one a route uses?

Next.js 15 renders a route statically by default and switches to dynamic rendering only when the route uses a request-time API or opts out explicitly.

The request-time APIs are `cookies()`, `headers()`, `draftMode()`, `connection()`, and reading `searchParams` in a page. Using any of them, or setting `export const dynamic = "force-dynamic"`, makes the route dynamic. Everything else is prerendered at build time.

One change from Next.js 14 trips people up: `fetch` is no longer cached by default. A route that calls an uncached `fetch` but uses no request-time APIs is still prerendered during the build. It just does not reuse that data on later regenerations unless you opt in. The [caching and revalidating guide](https://nextjs.org/docs/app/getting-started/caching-and-revalidating) and the [route segment config reference](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config) cover the exact rules.

## How do ISR, SSG and SSR compare?

The short version: SSG is the fastest and cheapest, ISR adds freshness at almost no speed cost, and SSR gives per-request data at the cost of speed and server time.

| | SSG | ISR | SSR (dynamic) |
| --- | --- | --- | --- |
| HTML generated | At build | At build, then in the background | On every request |
| Time to first byte | Lowest, served from CDN | Lowest, served from CDN | Depends on server and data |
| Content freshness | Until next deploy | Seconds to hours, or on demand | Always current |
| Server cost per visit | None | Only on regeneration | Every request |
| Can read cookies or headers | No | No | Yes |
| Build time with 10,000 pages | Long | Short if you prerender only the top pages | Short |
| Typical pages | Docs, about, case studies | Blog, product listings, CMS pages | Dashboards, carts, account pages |
| App Router API | `generateStaticParams` | `revalidate`, `revalidatePath`, `revalidateTag` | `cookies()`, `headers()`, `dynamic = "force-dynamic"` |

## How do I build a page with SSG in the App Router?

Export `generateStaticParams` from a dynamic route so Next.js knows every param at build time, and avoid request-time APIs in the page.

```tsx
// app/work/[slug]/page.tsx

export const dynamicParams = false; // unknown slugs return 404 instead of rendering on demand

export async function generateStaticParams() {
  const items = await getAllWork();
  return items.map((item) => ({ slug: item.slug }));
}

export default async function WorkPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const item = await getWorkItem(slug);
  if (!item) notFound();

  return (
    <article>
      <h1>{item.title}</h1>
      <p>{item.summary}</p>
    </article>
  );
}
```

`dynamicParams = false` is worth setting when the list is complete at build time, such as case studies stored as MDX files. Without it, a request for an unknown slug renders on demand and only then hits your `notFound()` call. See the [generateStaticParams reference](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) for the options.

## How do I add ISR to a page?

Export a `revalidate` value in seconds from the page or layout, or call `revalidatePath` or `revalidateTag` when your data changes.

Time-based ISR is one line:

```tsx
// app/blog/page.tsx

export const revalidate = 3600; // regenerate at most once per hour

export default async function BlogIndex() {
  const posts = await getPublishedPosts();
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.slug}>
          <Link href={`/blog/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  );
}
```

The value must be statically analysable by Next.js at build time, so write a plain literal such as `3600`. An expression like `60 * 60` or a variable imported from another file is not accepted.

On-demand ISR is better when a CMS or admin panel knows exactly when content changes. A webhook calls a route handler, and the handler invalidates the affected path:

```ts
// app/api/revalidate/route.ts

export async function POST(request: NextRequest) {
  if (request.headers.get("x-revalidate-secret") !== process.env.REVALIDATE_SECRET) {
    return NextResponse.json({ ok: false }, { status: 401 });
  }

  const body = (await request.json()) as { path?: string };
  if (!body.path) {
    return NextResponse.json({ ok: false, error: "path is required" }, { status: 400 });
  }

  revalidatePath(body.path);
  return NextResponse.json({ ok: true, revalidated: body.path });
}
```

This is the setup behind the [headless WordPress and Next.js concept](/work/headless-wordpress-nextjs), where publishing a post in WordPress refreshes the matching Next.js page within seconds. You can see it in my [headless WordPress with Next.js demo](/lab/headless-wordpress-nextjs), a blog that pulls posts from the WordPress REST API and rebuilds pages on publish.

If your data comes from `fetch`, tag it with `next: { tags: ["posts"] }` and call `revalidateTag("posts")` instead. One call then refreshes every page that used that data, which is easier than tracking paths. The API is documented under [revalidatePath](https://nextjs.org/docs/app/api-reference/functions/revalidatePath).

I usually combine both: on-demand revalidation for instant updates, plus a long time-based `revalidate`, such as a day, as a safety net in case a webhook is ever missed. That way a failed webhook means content is late, not stuck.

## How do I force SSR (dynamic rendering)?

Read a request-time API such as `cookies()` or `headers()`, or set `export const dynamic = "force-dynamic"` on the route.

In Next.js 15 these APIs are async, so you await them:

```tsx
// app/dashboard/page.tsx

export default async function DashboardPage() {
  const cookieStore = await cookies();
  const session = cookieStore.get("session")?.value;
  const user = session ? await getUserBySession(session) : null;
  if (!user) redirect("/login");

  return <h1>Welcome back, {user.name}</h1>;
}
```

Reading `cookies()` alone makes this route dynamic, so `force-dynamic` is not needed here. I add `force-dynamic` only when a page has no request-time API but still must never be cached, such as a page showing live stock levels from a database.

## How do I check which mode I actually got?

Run `next build` and read the route table, where Next.js marks each route as static, SSG or dynamic.

In Next.js 15 the legend uses an empty circle for static routes, a filled circle for routes prerendered through `generateStaticParams`, and an `ƒ` for dynamic routes rendered on demand. Routes with a `revalidate` value are listed with their revalidation interval, so you can confirm ISR is active. I check this table on every pull request that touches data loading, because one stray `headers()` call in a shared component can turn the whole blog dynamic without any visible error.

## Can I mix static and dynamic parts on one page?

Yes, keep the page static and move the per-user part into a client component or a separate request, so most of the HTML still comes from the cache.

A product page is a good example. The title, description and images are the same for everyone and can be ISR. The "in your cart" badge depends on the visitor. Rendering that badge in a client component that fetches from a route handler keeps the product page cacheable. Next.js 15 also has Partial Prerendering behind an experimental flag, which does this split automatically around `Suspense` boundaries, but I would not rely on an experimental flag for a production site yet.

## How do I decide between ISR, SSG and SSR?

Answer these questions in order and stop at the first yes.

1. Does the page show something specific to the visitor, such as their name, cart or permissions? Use SSR, or a static page with a client component for that part.
2. Does the page depend on query parameters that create unlimited combinations, such as search? Use SSR and mark it noindex.
3. Does the content change without a code deploy, from a CMS, database or API? Use ISR. Prefer on-demand revalidation if the source can send a webhook, otherwise a time-based `revalidate`.
4. Does the content only change when you deploy? Use SSG.

For SEO, any of the three is indexable. What changes is speed. A static or ISR page served from a CDN gives the lowest time to first byte, which is the base of a good LCP. I go through the other ranking basics in the [Next.js App Router SEO checklist](/blog/nextjs-app-router-seo-checklist).

For a SaaS product like my [SaaS analytics dashboard concept](/work/saas-analytics-dashboard), the usual split is SSG for marketing pages, ISR for a changelog, and dynamic rendering for everything behind login. A shop like the [e-commerce storefront concept](/work/ecommerce-storefront) fits ISR for product pages, with on-demand revalidation when stock or price changes, and dynamic rendering only for the cart and checkout. More Next.js work is on my [Next.js skill page](/skills/nextjs).

## What should you do first?

Run `next build` on your current project and read the route table. For every route marked dynamic, find the line that made it dynamic and ask whether the page really needs it. Most of the time, a blog, docs page or case study can drop to static or ISR with one change, and your slowest pages become your fastest without touching the design.

## FAQ

### Is ISR better than SSR for SEO?

For indexing, both are equal because both return complete HTML to the crawler. ISR usually wins on speed, since visitors and bots get a cached page from the CDN instead of waiting for a server render and database query. That lower time to first byte helps Largest Contentful Paint. Choose SSR only when the content truly depends on the request, such as cookies or location.

### How do I know if my Next.js page is static or dynamic?

Run next build and read the route table. Next.js marks each route with a symbol: an empty circle for static, a filled circle for SSG routes prerendered with generateStaticParams, and the f symbol for dynamic routes rendered on demand. If a page you expected to be static shows as dynamic, look for cookies, headers, searchParams or an uncached data call inside it.

### Does fetch cache by default in Next.js 15?

No. Next.js 15 changed fetch so it is not cached by default, reversing the behaviour of Next.js 14. A route can still be prerendered at build time if it uses no dynamic APIs, but the data will not be reused between revalidations unless you opt in with cache force-cache, a next.revalidate value, or a route-level revalidate export.

### What happens when an ISR page regenerates and the data source fails?

Next.js keeps serving the last successfully generated page. If the background render throws an error, the old HTML stays in the cache and Next.js tries again on a later request after the revalidate window. Visitors never see the error page from a failed regeneration, which makes ISR more forgiving than SSR when an API or database has a short outage.
