mcpField
N
nextjs-caching

nextjs-caching

github.com/josiahsiegel開発5972 回インストール19 週間前に更新
リポジトリ

nextjs-caching について

キャッシュと再検証を設定するNext.js開発者向けのリファレンススキルです。キャッシュ層、fetchの方式、オンデマンド無効化を例とともに説明し、挙動の調査にも役立ちます。

主な機能

  1. 01Next.jsの4つのキャッシュ層
  2. 02fetchのキャッシュと再検証オプション
  3. 03時間・タグベースの無効化
  4. 04静的生成とISRの指針
  5. 05キャッシュのデバッグ方法

活用シーン

  1. 01fetch呼び出しのキャッシュ方式選択
  2. 02更新後のコンテンツ再検証
  3. 03静的ページとISRの設定
  4. 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 を使用するコンテンツサイトでは時間ベースの再検証を使用します

コメント