cache-components について
cache-componentsは、Cache Componentsを有効にしたNext.jsプロジェクト向けの開発スキルです。キャッシュAPIとSuspense境界を使い、静的・キャッシュ済み・動的なコンテンツを組み合わせる方法を説明します。
主な機能
- 01Cache Components設定の検出
- 02'use cache'ディレクティブの利用ガイド
- 03キャッシュ期間とタグ無効化API
- 04静的シェルと動的ストリーミング
- 05ビルド時チェックとコードレビュー指針
活用シーン
- 01サーバー側のデータ取得へのキャッシュ追加
- 02キャッシュ済み・動的コンテンツを組み合わせたページ構成
- 03データ変更後のキャッシュ無効化
- 04Cache Componentsを使うコードベースのレビュー
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ zoonk/ zoonk --skill cache-componentsインストール後、Claude Code で/cache-componentsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
PATTERNS.mdREFERENCE.mdTROUBLESHOOTING.md詳細
Next.js Cache Components
自動有効化:next.config で
cacheComponents: trueが設定されているプロジェクトでは、このスキルが自動的に有効になります。
プロジェクトの検出
Next.js プロジェクトで作業を開始する際は、Cache Components が有効になっているか確認します。
# Check next.config.ts or next.config.js for cacheComponents
grep -r "cacheComponents" next.config.* 2>/dev/null
cacheComponents: true が見つかった場合、以下の作業ではこのスキルのパターンを積極的に適用します。
- React Server Components の記述
- データ取得の実装
- ミューテーションを行う Server Actions の作成
- ページのパフォーマンス最適化
- 既存のコンポーネントコードのレビュー
Cache Components は Partial Prerendering(PPR) を有効にし、静的 HTML シェルと動的なストリーミングコンテンツを組み合わせて、最適なパフォーマンスを実現します。
考え方:設定よりコード
Cache Components によって、セグメント設定から合成可能なコードへと移行します。
| 以前(非推奨) | 以後(Cache Components) |
|---|---|
export const revalidate = 3600 | 'use cache' 内の cacheLife('hours') |
export const dynamic = 'force-static' | 'use cache' と Suspense 境界を使用 |
| 静的か動的かの二者択一 | 細分化:静的シェル + キャッシュ済み + 動的 |
重要な原則:コンポーネントはデータだけでなく、キャッシュ処理もその場にまとめます。Next.js はビルド時にフィードバックを提供し、最適なパターンを選べるようにします。
中核となる概念
┌─────────────────────────────────────────────────────┐
│ Static Shell │
│ (Sent immediately to browser) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Header │ │ Cached │ │ Suspense │ │
│ │ (static) │ │ Content │ │ Fallback │ │
│ └─────────────┘ └─────────────┘ └──────┬──────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ Dynamic │ │
│ │ (streams) │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────┘
メンタルモデル:キャッシュの判断フロー
React Server Component を記述する際は、次の質問を順番に確認します。
┌─────────────────────────────────────────────────────────┐
│ Does this component fetch data or perform I/O? │
└─────────────────────┬───────────────────────────────────┘
│
┌──────────▼──────────┐
│ YES │ NO → Pure component, no action needed
└──────────┬──────────┘
│
┌─────────────────▼─────────────────┐
│ Does it depend on request context? │
│ (cookies, headers, searchParams) │
└─────────────────┬─────────────────┘
│
┌────────────┴────────────┐
│ │
┌────▼────┐ ┌─────▼─────┐
│ YES │ │ NO │
└────┬────┘ └─────┬─────┘
│ │
│ ┌─────▼─────────────────┐
│ │ Can this be cached? │
│ │ (same for all users?) │
│ └─────┬─────────────────┘
│ │
│ ┌──────────┴──────────┐
│ │ │
│ ┌────▼────┐ ┌─────▼─────┐
│ │ YES │ │ NO │
│ └────┬────┘ └─────┬─────┘
│ │ │
│ ▼ │
│ 'use cache' │
│ + cacheTag() │
│ │
└──────────────┬─────────────────────┘
│
▼
Wrap in <Suspense>
(dynamic streaming)
重要なポイント:'use cache' ディレクティブは、ユーザー間で同一の データに使用します。ユーザー固有のデータは、Suspense を使って動的なままにします。
クイックスタート
Cache Components を有効にする
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;
基本的な使い方
// Cached component - output included in static shell
async function CachedPosts() {
"use cache";
const posts = await db.posts.findMany();
return <PostList posts={posts} />;
}
// Page with static + cached + dynamic content
export default async function BlogPage() {
return (
<>
<Header /> {/* Static */}
<CachedPosts /> {/* Cached */}
<Suspense fallback={<Skeleton />}>
<DynamicComments /> {/* Dynamic - streams */}
</Suspense>
</>
);
}
中核となる API
1. 'use cache' ディレクティブ
コードをキャッシュ可能として指定します。次の3つのレベルで適用できます。
// File-level: All exports are cached
"use cache";
export async function getData() {
/* ... */
}
export async function Component() {
/* ... */
}
// Component-level
async function UserCard({ id }: { id: string }) {
"use cache";
const user = await fetchUser(id);
return <Card>{user.name}</Card>;
}
// Function-level
async function fetchWithCache(url: string) {
"use cache";
return fetch(url).then((r) => r.json());
}
重要:キャッシュする関数はすべて async である必要があります。
2. cacheLife() - キャッシュ期間の制御
import { cacheLife } from "next/cache";
async function Posts() {
"use cache";
cacheLife("hours"); // Use a predefined profile
// Or custom configuration:
cacheLife({
stale: 60, // 1 min - client cache validity
revalidate: 3600, // 1 hr - start background refresh
expire: 86400, // 1 day - absolute expiration
});
return await db.posts.findMany();
}
定義済みプロファイル:'default'、'seconds'、'minutes'、'hours'、'days'、'weeks'、'max'
3. cacheTag() - 無効化用のタグ
import { cacheTag } from "next/cache";
async function BlogPosts() {
"use cache";
cacheTag("posts");
cacheLife("days");
return await db.posts.findMany();
}
async function UserProfile({ userId }: { userId: string }) {
"use cache";
cacheTag("users", `user-${userId}`); // Multiple tags
return await db.users.findUnique({ where: { id: userId } });
}
4. updateTag() - 即時の無効化
書き込み後の読み取りのセマンティクスに使用します。
"use server";
import { updateTag } from "next/cache";
export async function createPost(formData: FormData) {
await db.posts.create({ data: formData });
updateTag("posts"); // Client immediately sees fresh data
}
5. revalidateTag() - バックグラウンドでの再検証
stale-while-revalidate パターンに使用します。
"use server";
import { revalidateTag } from "next/cache";
export async function updatePost(id: string, data: FormData) {
await db.posts.update({ where: { id }, data });
revalidateTag("posts", "max"); // Serve stale, refresh in background
}
各パターンの使い分け
| コンテンツの種類 | API | 動作 |
|---|---|---|
| 静的 | ディレクティブなし | ビルド時にレンダリング |
| キャッシュ済み | 'use cache' | 静的シェルに含まれ、再検証される |
| 動的 | <Suspense> 内 | リクエスト時にストリーミングされる |
パラメーターの組み合わせとサブシェル
重要な概念:Cache Components では、Next.js は指定されたパラメーターのすべての組み合わせをレンダリングし、再利用可能なサブシェルを作成します。
// app/products/[category]/[slug]/page.tsx
export async function generateStaticParams() {
return [
{ category: "jackets", slug: "classic-bomber" },
{ category: "jackets", slug: "essential-windbreaker" },
{ category: "accessories", slug: "thermal-fleece-gloves" },
];
}
Next.js は次のルートをレンダリングします。
/products/jackets/classic-bomber ← Full params (complete page)
/products/jackets/essential-windbreaker ← Full params (complete page)
/products/accessories/thermal-fleece-gloves ← Full params (complete page)
/products/jackets/[slug] ← Partial params (category subshell)
/products/accessories/[slug] ← Partial params (category subshell)
/products/[category]/[slug] ← No params (fallback shell)
重要な理由:カテゴリーのサブシェル(/products/jackets/[slug])は、generateStaticParams に含まれていない商品も含め、任意のジャケット商品に再利用できます。一覧にないジャケットにユーザーが移動した場合、キャッシュ済みのカテゴリーシェルがすぐに表示され、商品の詳細はストリーミングで読み込まれます。
generateStaticParams の要件
Cache Components が有効な場合:
- 少なくとも1つのパラメーターを指定する必要があります - 空の配列はビルドエラーになります(本番環境でのサイレント障害を防止します)
- パラメーターによって静的に安全であることを証明します - パラメーターを指定すると、Next.js は動的 API が呼び出されていないことを検証できます
- 部分的なパラメーターによってサブシェルが作成されます - 一意の組み合わせごとに、再利用可能なシェルが生成されます
// ❌ ERROR with Cache Components
export function generateStaticParams() {
return []; // Build error: must provide at least one param
}
// ✅ CORRECT: Provide real params
export async function generateStaticParams() {
const products = await getPopularProducts();
return products.map(({ category, slug }) => ({ category, slug }));
}
キャッシュキー = 引数
引数はキャッシュキーの一部になります。
// Different userId = different cache entry
async function UserData({ userId }: { userId: string }) {
"use cache";
cacheTag(`user-${userId}`);
return await fetchUser(userId);
}
ビルド時のフィードバック
Cache Components は、開発中に早期のフィードバックを提供します。次のビルドエラーは、最適なパターンを選べるように導きます。
エラー:Suspense の外部に動的データがある
Error: Accessing cookies/headers/searchParams outside a Suspense boundary
解決策:動的なコンポーネントを <Suspense> で囲みます。
<Suspense fallback={<Skeleton />}>
<ComponentThatUsesCookies />
</Suspense>
エラー:Suspense の外部にキャッシュされていないデータがあります
Error: Accessing uncached data outside Suspense
解決策:データをキャッシュするか、Suspense でラップしてください。
// Option 1: Cache it
async function ProductData({ id }: { id: string }) {
"use cache";
return await db.products.findUnique({ where: { id } });
}
// Option 2: Make it dynamic with Suspense
<Suspense fallback={<Loading />}>
<DynamicProductData id={id} />
</Suspense>;
エラー:キャッシュ内でリクエストデータを使用しています
Error: Cannot access cookies/headers inside 'use cache'
解決策:ランタイムデータをキャッシュ境界の外に取り出してください(上記の「ランタイムデータの処理」を参照)。
追加リソース
- API リファレンスの全容については、REFERENCE.md を参照してください
- よく使われるパターンやレシピについては、PATTERNS.md を参照してください
- デバッグとトラブルシューティングについては、TROUBLESHOOTING.md を参照してください
コード生成ガイドライン
Cache Component のコードを生成する際は、次の点に従ってください。
- 必ず
asyncを使用する - キャッシュ対象の関数はすべて async である必要があります 'use cache'を先頭に置く - 関数本体の最初の文にする必要がありますcacheLife()を早めに呼び出す -'use cache'ディレクティブの直後に記述してください- 意味のあるタグを付ける - 無効化の要件に合った意味的なタグを使用してください
- ランタイムデータを取り出す -
cookies()/headers()をキャッシュ対象の範囲外に移動してください - 動的コンテンツをラップする - キャッシュされない非同期コンポーネントには
<Suspense>を使用してください
積極的な適用(Cache Components が有効な場合)
プロジェクトで cacheComponents: true が検出された場合は、これらのパターンを自動的に適用してください。
データ取得コンポーネントを記述する場合
「このデータはキャッシュできるか」と自問してください。できる場合は、'use cache' を追加してください。
// Before: Uncached fetch
async function ProductList() {
const products = await db.products.findMany();
return <Grid products={products} />;
}
// After: With caching
async function ProductList() {
"use cache";
cacheTag("products");
cacheLife("hours");
const products = await db.products.findMany();
return <Grid products={products} />;
}
Server Actions を記述する場合
ミューテーション後は、常に関連するキャッシュを無効化してください。
"use server";
import { updateTag } from "next/cache";
export async function createProduct(data: FormData) {
await db.products.create({ data });
updateTag("products"); // Don't forget!
}
ページを構成する場合
静的シェル、キャッシュされたコンテンツ、動的ストリーミングを組み合わせて構成してください。
export default async function Page() {
return (
<>
<StaticHeader /> {/* No cache needed */}
<CachedContent /> {/* 'use cache' */}
<Suspense fallback={<Skeleton />}>
<DynamicUserContent /> {/* Streams at runtime */}
</Suspense>
</>
);
}
コードをレビューする場合
Cache Components を使用するプロジェクトでは、次の問題を指摘してください。
- キャッシュが有効なデータ取得で
'use cache'が使用されていない -
cacheTag()の呼び出しがない(無効化ができなくなります) - Server Actions でミューテーション後に
updateTag()/revalidateTag()が呼び出されていない -
'use cache'のスコープ内でcookies()/headers()が呼び出されている - 動的コンポーネントに
<Suspense>境界がない - 非推奨:
export const revalidate-'use cache'内のcacheLife()に置き換えてください - 非推奨:
export const dynamic- Suspense とキャッシュ境界に置き換えてください -
generateStaticParams()の戻り値が空 - 少なくとも 1 つのパラメーターを指定する必要があります
注記
このプロジェクトでは、デフォルトの cacheLife に max プロファイルを設定しているため、キャッシュの有効期間をカスタマイズする必要がない限り、ほとんどの場合 cacheLife() を呼び出す必要はありません。