nextjs-caching について
キャッシュと再検証を設定するNext.js開発者向けのリファレンススキルです。キャッシュ層、fetchの方式、オンデマンド無効化を例とともに説明し、挙動の調査にも役立ちます。
主な機能
- 01Next.jsの4つのキャッシュ層
- 02fetchのキャッシュと再検証オプション
- 03時間・タグベースの無効化
- 04静的生成とISRの指針
- 05キャッシュのデバッグ方法
活用シーン
- 01fetch呼び出しのキャッシュ方式選択
- 02更新後のコンテンツ再検証
- 03静的ページとISRの設定
- 04古い、または予期しないキャッシュデータの調査
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ josiahsiegel/ claude-plugin-marketplace --skill nextjs-cachingインストール後、Claude Code で/nextjs-cachingを入力して読み込まれたか確認します。
詳細
クイックリファレンス
| キャッシュレイヤー | 場所 | 期間 | 目的 |
|---|---|---|---|
| リクエストのメモ化 | サーバー | リクエストごと | 同じフェッチの重複を排除 |
| データキャッシュ | サーバー | 永続 | フェッチ結果を保存 |
| フルルートキャッシュ | サーバー | 永続 | 事前レンダリングされた HTML/RSC |
| ルーターキャッシュ | クライアント | セッション | クライアント側のナビゲーション |
Next.js 16:'use cache' ディレクティブ
| ディレクティブ | スコープ | ユースケース |
|---|---|---|
'use cache' | デフォルトキャッシュ | 一般的なキャッシュ |
'use cache: remote' | CDN/エッジキャッシュ | 静的な共有コンテンツ |
'use cache: private' | ユーザーごとのキャッシュ | ユーザー固有のデータ |
| キャッシュ関数 | コード | 目的 |
|---|---|---|
cacheLife('hours') | 期間 | キャッシュの有効期間を設定 |
cacheTag('posts') | タグ付け | 対象を指定した再検証を有効化 |
フェッチオプション
| フェッチオプション | コード | 効果 |
|---|---|---|
| デフォルト | fetch(url) | 無期限にキャッシュ |
| キャッシュなし | { cache: 'no-store' } | 常に最新の状態 |
| 再検証 | { next: { revalidate: 60 } } | 60 秒後に古いデータになる |
| タグ | { next: { tags: ['posts'] } } | 無効化用のタグを設定 |
| 再検証 | コード | ユースケース |
|---|---|---|
revalidatePath('/posts') | パス | ミューテーション後 |
revalidateTag('posts') | タグ | タグ付きデータを無効化 |
router.refresh() | クライアント | 現在のルートを更新 |
このスキルを使用する場面
キャッシュと再検証に使用します:
- Next.js のキャッシュ動作を理解する
- フェッチのキャッシュ戦略を設定する
- インクリメンタル静的再生成(ISR)を設定する
- オンデマンド再検証を実装する
- キャッシュの問題をデバッグする
関連スキル:
- データフェッチについては、
nextjs-data-fetchingを参照してください - Server Actions については、
nextjs-server-actionsを参照してください - 静的生成については、
nextjs-deploymentを参照してください
Next.js のキャッシュ
キャッシュの概要
Next.js には、4 つのキャッシュ機構があります:
| 機構 | 対象 | 場所 | 目的 | 期間 |
|---|---|---|---|---|
| リクエストのメモ化 | 関数の戻り値 | サーバー | コンポーネントツリー内の重複リクエストを回避 | リクエストごと |
| データキャッシュ | データ | サーバー | リクエストやデプロイをまたいでデータを保存 | 永続 |
| フルルートキャッシュ | HTML および RSC ペイロード | サーバー | レンダリングコストを削減 | 永続 |
| ルーターキャッシュ | RSC ペイロード | クライアント | ナビゲーション時のサーバーリクエストを削減 | セッション |
リクエストのメモ化
自動重複排除
// This is automatically memoized - only one fetch happens
async function getUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`);
return res.json();
}
// Layout uses getUser
export default async function Layout({ children }: { children: React.ReactNode }) {
const user = await getUser('1'); // First call - fetches
return <div>{children}</div>;
}
// Page also uses getUser
export default async function Page() {
const user = await getUser('1'); // Second call - uses cached result
return <div>{user.name}</div>;
}
cache() を使った手動のメモ化
import { cache } from 'react';
// For non-fetch functions like database queries
export const getUser = cache(async (id: string) => {
return db.users.findUnique({ where: { id } });
});
// Both will use the same cached result
const user1 = await getUser('1');
const user2 = await getUser('1');
データキャッシュ
fetch() によるキャッシュ
// Cached indefinitely (default)
const data = await fetch('https://api.example.com/data');
// Opt out of caching
const data = await fetch('https://api.example.com/data', {
cache: 'no-store',
});
// Time-based revalidation
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }, // Revalidate every hour
});
// Tag-based caching
const data = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
});
fetch 以外での unstable_cache
import { unstable_cache } from 'next/cache';
const getCachedUser = unstable_cache(
async (userId: string) => {
return db.users.findUnique({ where: { id: userId } });
},
['user'], // Cache key parts
{
revalidate: 3600, // 1 hour
tags: ['users'],
}
);
export default async function UserPage({ params }: { params: { id: string } }) {
const user = await getCachedUser(params.id);
return <div>{user?.name}</div>;
}
ルートセグメントの設定
// Opt entire route out of caching
export const dynamic = 'force-dynamic';
// Set revalidation for entire route
export const revalidate = 60; // seconds
// Disable cache for all fetches in route
export const fetchCache = 'force-no-store';
// Force static generation
export const dynamic = 'force-static';
再検証
時間ベースの再検証
// Fetch with revalidation
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }, // Revalidate after 60 seconds
});
// Route-level revalidation
export const revalidate = 60;
オンデマンド再検証
// app/actions.ts
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
// Revalidate a specific path
export async function updatePost(id: string, data: FormData) {
await db.posts.update({ where: { id }, data: { /* ... */ } });
// Revalidate the specific post page
revalidatePath(`/posts/${id}`);
// Revalidate the posts list
revalidatePath('/posts');
}
// Revalidate by tag
export async function createPost(data: FormData) {
await db.posts.create({ data: { /* ... */ } });
// All fetches with 'posts' tag will be revalidated
revalidateTag('posts');
}
再検証用 API ルート
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const secret = request.headers.get('x-revalidate-secret');
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
}
const body = await request.json();
if (body.tag) {
revalidateTag(body.tag);
}
if (body.path) {
revalidatePath(body.path);
}
return NextResponse.json({ revalidated: true, now: Date.now() });
}
タグベースのキャッシュ
// Fetch with tags
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { tags: ['posts', 'content'] },
});
return res.json();
}
async function getPost(id: string) {
const res = await fetch(`https://api.example.com/posts/${id}`, {
next: { tags: ['posts', `post-${id}`] },
});
return res.json();
}
// Revalidate all posts
revalidateTag('posts');
// Revalidate specific post
revalidateTag('post-123');
// Revalidate all content
revalidateTag('content');
フルルートキャッシュ
静的ルート
// Automatically cached at build time
export default async function StaticPage() {
const data = await getData();
return <div>{data}</div>;
}
// Force static
export const dynamic = 'force-static';
generateStaticParams を使った動的ルート
// app/posts/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await db.posts.findMany({ select: { slug: true } });
return posts.map((post) => ({ slug: post.slug }));
}
// Pages are pre-rendered at build time
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return <article>{post.content}</article>;
}
キャッシュのオプトアウト
// Dynamic route - not cached
export const dynamic = 'force-dynamic';
// Or use dynamic functions
import { headers, cookies } from 'next/headers';
export default async function Page() {
const headersList = await headers(); // Makes route dynamic
return <div>...</div>;
}
ルーターキャッシュ(クライアント側)
プリフェッチ
import Link from 'next/link';
// Prefetch on hover (default)
<Link href="/about">About</Link>
// Disable prefetching
<Link href="/dashboard" prefetch={false}>Dashboard</Link>
ルーターキャッシュの無効化
'use client';
import { useRouter } from 'next/navigation';
export function RefreshButton() {
const router = useRouter();
const handleRefresh = () => {
// Refresh current route and invalidate cache
router.refresh();
};
return <button onClick={handleRefresh}>Refresh</button>;
}
revalidatePath とルーターキャッシュ
// Server Action
'use server';
import { revalidatePath } from 'next/cache';
export async function updateData() {
// Update data in database
await db.data.update({ /* ... */ });
// This also clears the Router Cache for this path
revalidatePath('/data');
}
キャッシュヘッダー
ルートハンドラーのキャッシュ
// app/api/data/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
const data = await fetchData();
return NextResponse.json(data, {
headers: {
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
},
});
}
動的ルートでの generateStaticParams
// Generate some pages statically, allow others dynamically
export async function generateStaticParams() {
// Only pre-render the top 100 posts
const posts = await db.posts.findMany({ take: 100 });
return posts.map((post) => ({ slug: post.slug }));
}
// Allow dynamic rendering for posts not in generateStaticParams
export const dynamicParams = true; // default
// Or 404 for unknown slugs
export const dynamicParams = false;
キャッシュ戦略
Stale-While-Revalidate パターン
// Serve cached content while revalidating in background
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 },
});
// First request after 60s: serves stale, triggers revalidation
// Subsequent requests: serve fresh data
キャッシュ優先戦略
// Cache indefinitely, revalidate on-demand
const data = await fetch('https://api.example.com/static-data');
// Use revalidateTag('static-data') when data changes
ネットワーク優先戦略
// Always fetch fresh data
const data = await fetch('https://api.example.com/realtime', {
cache: 'no-store',
});
キャッシュのデバッグ
キャッシュステータスヘッダー
// Enable cache debugging
// next.config.js
module.exports = {
logging: {
fetches: {
fullUrl: true,
},
},
};
キャッシュヒット/ミスの確認
// In development, check terminal for cache status
// HIT - served from cache
// MISS - fetched from origin
// STALE - served stale, revalidating
ベストプラクティス
| プラクティス | 説明 |
|---|---|
| 関連データにはタグを使用する | 関連するフェッチをタグでグループ化します |
| 再検証は必要最小限にする | 変更されたデータのみを再検証します |
| 静的生成を優先する | 可能な場合は generateStaticParams を使用します |
| データベースクエリをキャッシュする | fetch 以外には unstable_cache を使用します |
| 適切な TTL を設定する | 鮮度とパフォーマンスのバランスを取ります |
| コンテンツには ISR を使用する | コンテンツサイトでは時間ベースの再検証を使用します |
コメント
Markdown 対応