[2026] Next.js App Router: SSR, SSG, and ISR | Rendering Strategy and Caching

[2026] Next.js App Router: SSR, SSG, and ISR | Rendering Strategy and Caching

이 글의 핵심

Choose SSR, SSG, and ISR in the Next.js App Router: fetch cache, revalidate, Route Segment Config, and practical criteria for content and personalization.

Introduction

In the Next.js App Router (13+), how each page is built on the server and how long it is cached drives performance, SEO, and cost. Stopping at Next.js App Router SSR vs SSG names alone makes it easy to hit unintended caching from a single fetch line. This post maps static generation, server rendering, and incremental revalidation (ISR) on top of the default server components model, and shows how to control behavior consistently with revalidate and Route Segment Config. It assumes App Router + fetch cache semantics (2026). In production, CDN, data cache, and server component caches stack—“docs say X but I see old data” is common. Below: concepts → code → advanced settings → selection criteria → cases → troubleshooting.

After reading this post

  • Distinguish what SSR, SSG, and ISR mean in the App Router
  • Design per-fetch caching with fetch options and revalidate
  • Split strategies for dynamic routes, personalization, and admin UIs

Table of contents

  1. Concepts
  2. Hands-on implementation
  3. Advanced: Route Segment Config
  4. Performance comparison
  5. Real-world cases
  6. Troubleshooting
  7. Wrap-up

Concepts

Terminology (vs Pages Router intuition)

TermIntuitive meaningApp Router reality
SSGHTML at build (or regen) timePaths where server component trees can be cached statically—e.g. fetch(..., { cache: 'force-cache' })
SSRHTML per requestDynamic rendering—closer to cache: 'no-store' or dynamic = 'force-dynamic'
ISRRefresh static output on a schedule or on demandfetch revalidate seconds or revalidatePath / revalidateTag
The App Router creates cache boundaries from fetch calls and segment settings, not a single “page mode” switch.

React Server Components (RSC)

Server components run on the server by default; the client receives serialized output. “SSR or SSG” becomes when and how that result is cached.

Hands-on implementation

2-1. SSG-like: fixed data at build

아래 코드는 typescript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// app/posts/page.tsx — cache at build time (force-cache)
export default async function PostsPage() {
  const res = await fetch('https://api.example.com/posts', {
    cache: 'force-cache',
  });
  const posts = await res.json();
  return (
    <ul>
      {posts.map((p: { id: string; title: string }) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  );
}

2-2. SSR: fresh data every request

아래 코드는 typescript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// app/dashboard/page.tsx
export default async function DashboardPage() {
  const res = await fetch('https://api.example.com/me', {
    cache: 'no-store',
  });
  const user = await res.json();
  return <div>{user.name}</div>;
}

2-3. ISR: time-based revalidation

아래 코드는 typescript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// app/blog/[slug]/page.tsx
export default async function PostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { revalidate: 3600 }, // background revalidate every hour
  });
  if (!res.ok) notFound();
  const post = await res.json();
  return <article>{post.body}</article>;
}

2-4. Tag-based invalidation (operations)

아래 코드는 typescript를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// on fetch
await fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] },
});
// in a Server Action or Route Handler
import { revalidateTag } from 'next/cache';
export async function POST() {
  revalidateTag('posts');
  return Response.json({ ok: true });
}

Summary: one route can mix strategies if different components use different fetch settings. A per-data-source cache policy table reduces mistakes.

Advanced: Route Segment Config and static/dynamic boundaries

Force dynamic at segment level

// app/admin/layout.tsx
export const dynamic = 'force-dynamic';
export const fetchCache = 'force-no-store';
  • dynamic: 'auto' | 'force-dynamic' | 'error' | 'force-static' — default tendency for the route
  • revalidate: segment-level default ISR interval (understand together with route-wide revalidate)

generateStaticParams for SSG scope

아래 코드는 typescript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const slugs = await getAllSlugs(); // build-time list
  return slugs.map((slug) => ({ slug }));
}

If a CMS exports tens of thousands of slugs, do not prerender all at build—top N static, rest on-demand ISR.

Performance comparison

ScenarioDirectionWhy
Marketing landing, docs, legalSSG + long revalidate or fully staticMax CDN hit rate, stable TTFB
Product dashboard, cartSSR (no-store) or client splitPer-user data; avoid cache poisoning
Blog, catalogISR + revalidateTagBalance traffic vs freshness
Real-time stock/priceSSR + short TTL or Edge + external cacheNext cache alone may not be enough
Key: prefer agreeing “how stale can this fetch be?” over labeling a whole page “SSG.”

Real-world cases

  • E-commerce listing: list revalidate: 300 + tag products; on price change revalidateTag('products').
  • Post-login header: cache: 'no-store' for user info; keep shared nav static in a separate slice to minimize personalization surface.
  • Docs site: mostly SSG; search via client or separate API—split rendering from search indexing.

Troubleshooting

“Build is fresh; production shows stale data”

Check fetch cache vs revalidate, and host/CDN data cache settings.

revalidatePath did nothing”

Verify the same segment tree and cache keys; tag-based invalidation is often more reliable.

“Almost leaked personal data across users”

Never force-cache per-user APIs. Session/cookie-sensitive fetch should use no-store or a separate permission boundary.

Wrap-up

SSR vs SSG in the App Router is best understood as the cache story from fetch and segment config, not labels alone. Document per-layer cache standards (TTL, tags, invalidation triggers) so teams spend less time re-debating performance vs freshness. Pair with the Node.js performance post for async and server load.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3