DevToolBox無料
ブログ

Next.js キャッシュ戦略:ISR、SSG、Fetch Cache

13分by DevToolBox

Next.js Caching Strategies: ISR, SSG, Fetch Cache, and Data Cache Explained

Next.js has one of the most sophisticated caching systems of any web framework. Understanding when data is cached, how long it is cached, and how to invalidate it is essential for building fast, correct applications. This guide covers every caching layer in Next.js 14/15 — from static generation to on-demand revalidation — with practical examples and common pitfalls.

The Four Caching Layers

LayerWhat It CachesWhereDurationInvalidation
Request Memoizationfetch() return valuesServer (per request)Single request lifecycleAutomatic
Data Cachefetch() responsesServer (persistent)Indefinite or time-basedrevalidatePath / revalidateTag
Full Route CacheRendered HTML + RSC payloadServer (persistent)Until revalidationrevalidatePath / revalidateTag
Router CacheRSC payloadsClient (in-memory)Session or time-basedrouter.refresh() / revalidatePath

Static Site Generation (SSG)

SSG renders pages at build time. The HTML is generated once and served from a CDN. This is the fastest option but only works for content that does not change between builds.

// app/blog/[slug]/page.tsx — fully static at build time

// Generate all possible paths at build time
export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

// This page is rendered once at build time
export default async function BlogPost({
  params,
}: {
  params: { slug: string };
}) {
  const post = await getPost(params.slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.publishedAt}</time>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

// Force static rendering
export const dynamic = 'force-static';
export const revalidate = false; // Never revalidate — pure SSG

Incremental Static Regeneration (ISR)

ISR combines the speed of static pages with the freshness of dynamic content. Pages are served statically but regenerated in the background after a configurable time period.

// app/products/[id]/page.tsx — ISR with time-based revalidation

// Revalidate this page every 60 seconds
export const revalidate = 60;

export default async function ProductPage({
  params,
}: {
  params: { id: string };
}) {
  // This fetch is cached for 60 seconds
  const product = await fetch(
    `https://api.example.com/products/${params.id}`,
    { next: { revalidate: 60 } }
  ).then((r) => r.json());

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <span>Price: ${product.price}</span>
    </div>
  );
}

// How ISR works:
// 1. First request → generates static page, serves it
// 2. Subsequent requests within 60s → serves cached static page
// 3. Request after 60s → serves stale page, triggers regeneration in background
// 4. Next request → serves newly generated page

On-Demand ISR with revalidateTag

// app/products/[id]/page.tsx — tag-based revalidation
export default async function ProductPage({
  params,
}: {
  params: { id: string };
}) {
  const product = await fetch(
    `https://api.example.com/products/${params.id}`,
    {
      next: {
        tags: [`product-${params.id}`, 'products'],  // Tag for invalidation
      },
    }
  ).then((r) => r.json());

  return (
    <div>
      <h1>{product.name}</h1>
      <p>Stock: {product.stock}</p>
    </div>
  );
}

// app/api/revalidate/route.ts — webhook endpoint
import { revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const { tag, secret } = await request.json();

  // Verify webhook secret
  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
  }

  // Revalidate specific product
  revalidateTag(tag);  // e.g., 'product-42'

  // Or revalidate all products
  // revalidateTag('products');

  return NextResponse.json({ revalidated: true, tag });
}

// app/api/revalidate-path/route.ts — path-based revalidation
import { revalidatePath } from 'next/cache';

export async function POST(request: NextRequest) {
  const { path } = await request.json();

  // Revalidate a specific path
  revalidatePath(path);             // e.g., '/products/42'

  // Revalidate all pages under a layout
  revalidatePath('/products', 'layout');

  // Revalidate everything
  revalidatePath('/', 'layout');

  return NextResponse.json({ revalidated: true });
}

Fetch Cache Configuration

// Different caching strategies per fetch call

// Cache indefinitely (default in App Router)
const data = await fetch('https://api.example.com/config');

// Cache with time-based revalidation
const products = await fetch('https://api.example.com/products', {
  next: { revalidate: 300 },  // Revalidate every 5 minutes
});

// No cache — always fetch fresh data
const user = await fetch('https://api.example.com/user/me', {
  cache: 'no-store',  // Never cache
});

// Cache with tags for on-demand revalidation
const post = await fetch(`https://api.example.com/posts/${id}`, {
  next: { tags: [`post-${id}`, 'posts'] },
});

// Force cache (useful when dynamic is set on the route)
const settings = await fetch('https://api.example.com/settings', {
  cache: 'force-cache',
});

Request Memoization

// Next.js automatically deduplicates identical fetch calls
// within a single request/render cycle

// layout.tsx
export default async function Layout({ children }) {
  const user = await fetch('/api/user/me');  // Fetch #1
  return (
    <div>
      <Header user={user} />
      {children}
    </div>
  );
}

// page.tsx (child of layout)
export default async function Page() {
  const user = await fetch('/api/user/me');  // Same URL = memoized, NOT a second request
  return <Profile user={user} />;
}

// Both components get the same data from a single network request
// This only works for GET requests with the same URL and options

// Opt out of memoization
const data = await fetch('/api/data', {
  signal: AbortSignal.timeout(5000),  // Adding signal opts out of memoization
});

Server Actions and Cache Invalidation

// app/products/[id]/actions.ts
'use server';

import { revalidateTag, revalidatePath } from 'next/cache';

export async function updateProduct(id: string, formData: FormData) {
  const name = formData.get('name') as string;
  const price = Number(formData.get('price'));

  // Update in database
  await db.products.update({
    where: { id },
    data: { name, price },
  });

  // Invalidate the product's cached data
  revalidateTag(`product-${id}`);

  // Or invalidate the specific path
  revalidatePath(`/products/${id}`);
}

export async function deleteProduct(id: string) {
  await db.products.delete({ where: { id } });

  // Invalidate multiple caches
  revalidateTag(`product-${id}`);
  revalidateTag('products');         // Product listing pages
  revalidatePath('/products');       // Products index page
}

// app/products/[id]/page.tsx — using the action
import { updateProduct } from './actions';

export default async function ProductPage({ params }) {
  const product = await getProduct(params.id);

  return (
    <form action={updateProduct.bind(null, params.id)}>
      <input name="name" defaultValue={product.name} />
      <input name="price" type="number" defaultValue={product.price} />
      <button type="submit">Update</button>
    </form>
  );
}

unstable_cache for Database Queries

// When you are not using fetch (e.g., direct database queries),
// use unstable_cache to opt into the Data Cache

import { unstable_cache } from 'next/cache';
import { db } from '@/lib/db';

const getCachedProducts = unstable_cache(
  async (category: string) => {
    return db.products.findMany({
      where: { category },
      orderBy: { createdAt: 'desc' },
    });
  },
  ['products'],           // Cache key parts
  {
    tags: ['products'],   // Tags for revalidation
    revalidate: 300,      // Revalidate every 5 minutes
  }
);

// Usage in a Server Component
export default async function ProductsPage({
  searchParams,
}: {
  searchParams: { category?: string };
}) {
  const products = await getCachedProducts(
    searchParams.category || 'all'
  );

  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>{p.name} — ${p.price}</li>
      ))}
    </ul>
  );
}

// Invalidate with:
// revalidateTag('products');

Dynamic Rendering

// Force dynamic rendering for pages that need fresh data every request

// Option 1: Route segment config
export const dynamic = 'force-dynamic';

// Option 2: Using cookies or headers (auto-detected)
import { cookies, headers } from 'next/headers';

export default async function DashboardPage() {
  const cookieStore = cookies();           // Triggers dynamic rendering
  const headersList = headers();           // Also triggers dynamic rendering
  const token = cookieStore.get('session');

  const data = await fetch('/api/dashboard', {
    headers: { Authorization: `Bearer ${token?.value}` },
    cache: 'no-store',
  });

  return <Dashboard data={data} />;
}

// Option 3: Using searchParams
export default async function SearchPage({
  searchParams,
}: {
  searchParams: { q?: string };          // Using searchParams triggers dynamic
}) {
  const results = await search(searchParams.q);
  return <SearchResults results={results} />;
}

Caching Decision Flowchart

Is the data the same for every user?
├── YES → Is it updated frequently?
│   ├── NO → SSG (revalidate: false)
│   └── YES → ISR (revalidate: N seconds)
│       └── Need instant updates? → On-demand revalidation (revalidateTag)
└── NO → Is it sensitive/personalized?
    ├── YES → Dynamic rendering (cache: 'no-store')
    └── NO → ISR with user-specific cache keys

Common Pitfalls

  • Forgetting cache: no-store for auth — user-specific data will show stale content to wrong users without it
  • Not tagging fetches — without tags, you cannot do surgical on-demand revalidation
  • Caching too aggressively — default App Router behavior caches everything; be explicit about what should be dynamic
  • Router Cache staleness — client-side cache can serve stale data; use router.refresh() when needed
  • Missing revalidation in Server Actions — always call revalidateTag/Path after mutations
  • Confusing revalidate values — route-level revalidate is a minimum; fetch-level revalidate can be shorter

Production Configuration

// next.config.ts — caching-related settings
import type { NextConfig } from 'next';

const config: NextConfig = {
  // Enable ISR for all pages by default
  experimental: {
    // PPR (Partial Prerendering) — Next.js 15+
    ppr: true,
  },

  // Custom cache handler for multi-instance deployments
  cacheHandler: process.env.NODE_ENV === 'production'
    ? require.resolve('./cache-handler.mjs')
    : undefined,
  cacheMaxMemorySize: 0,  // Disable in-memory cache (use Redis instead)

  // Headers for CDN caching
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=0, s-maxage=60, stale-while-revalidate=300',
          },
        ],
      },
      {
        source: '/api/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'no-store',
          },
        ],
      },
    ];
  },
};

export default config;

Test your API cache headers with our HTTP Status Code Lookup tool. For understanding the JSON payloads in your cached responses, use our JSON Formatter. For more on Next.js architecture, check out our Next.js Middleware guide.

𝕏 Twitterin LinkedIn
この記事は役に立ちましたか?

最新情報を受け取る

毎週の開発ヒントと新ツール情報。

スパムなし。いつでも解除可能。

Try These Related Tools

{ }JSON Formatter🔄cURL to Code Converter

Related Articles

Next.js SEO最適化完全ガイド 2026

Next.js SEOをマスター。

Next.js App Router: 2026年完全移行ガイド

Next.js App Routerの包括的ガイド。Server Components、データフェッチ、レイアウト、ストリーミング、Server Actions、Pages Routerからの移行手順を解説。

Webパフォーマンス最適化:2026 Core Web Vitalsガイド

Webパフォーマンス最適化とCore Web Vitalsの完全ガイド。画像、JavaScript、CSS、キャッシュの実践テクニックでLCP、INP、CLSを改善する方法を学ぶ。