Next.js: キャッシュコンポーネント & use cache
最終更新:2026-08-26
use cacheは Next.js 16 で最も革新的な API です — キャッシュを「データ取得の副作用」から「コンポーネントレベルのファーストクラスシチズン」に引き上げます。
1. 学習目標
use cache()ディレクティブとコンテンツキャッシュの概念cacheTag()によるキャッシュコンテンツのタグ付けcacheLife()によるキャッシュ有効期限の設定revalidateTag()によるオンデマンドキャッシュ無効化- 暗黙的な fetch キャッシュと明示的な
use cacheキャッシュの違い
2. あるシステムアーキテクトの実話
(1) 課題: 同じページで4回の同一 API 呼び出し
TaskFlow ダッシュボードをレビュー中、Charlie はページ上の4つのコンポーネント — <UserAvatar>、<UserGreeting>、<UserStats>、<UserNotifications> — がそれぞれ fetch('/api/user') を呼び出していることに気づきました。同じ URL に対するキャッシュヒットは正常に動作していましたが、データベースクエリ関数 getUserFromDB() は4回呼び出されていました。fetch キャッシュは HTTP リクエストにのみ適用され、サーバー内部の関数呼び出しに対してはまったく効果がありません。
| 問題 | データ |
|---|---|
| ページレベルのデータベース呼び出し | 4回の同一クエリ |
| 1クエリあたりの時間 | 200 ms |
| 追加の合計時間 | 600 ms 無駄 |
| データベース QPS の無駄 | 4倍 |
(2) use cache による解決策
関数を
use cache()でラップします — これにより、データベースクエリ、計算、ファイル読み取りを含む、あらゆる内部関数呼び出しがキャッシュされます。
// app/dashboard/page.tsx
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
async function getUserData() {
'use cache'
cacheTag('user-data')
cacheLife({ stale: 300, revalidate: 600 })
const user = await db.user.findUnique({ where: { id: 1 } }) // 1回だけ実行
return user
}
export default async function DashboardPage() {
const user = await getUserData() // 最初の呼び出しでデータベースクエリを実行
// 後続の呼び出しはキャッシュされた結果を直接返します
return (
<div>
<UserAvatar user={user} />
<UserGreeting user={user} />
<UserStats user={user} />
<UserNotifications user={user} />
</div>
)
}
(3) 成果
| 指標 | 従来モデル | use cache |
|---|---|---|
| データベースクエリ数 | 4 | 1 |
| 追加遅延 | 600 ms | 0 ms |
| キャッシュ粒度 | URL レベル | 関数/コンポーネントレベル |
| カスタムロジックキャッシュ | ❌ 非対応 (HTTP fetch のみ) | ✅ 任意のコードに対応 |
3. use cache() ディレクティブとコンテンツキャッシュ
use cache は関数レベルのディレクティブです — 関数の先頭に 'use cache' を追加すると、その関数の結果がキャッシュされることを示します。キャッシュされた内容は コンテンツキャッシュ と呼ばれ、Next.js 16 で導入されたまったく新しいキャッシュレイヤーで、従来のデータキャッシュ (fetch キャッシュ) とは独立しています。
graph TB
subgraph "Next.js 16 キャッシュシステム"
A[リクエストメモ化<br/>リクエストレベルのメモリ]
B[データキャッシュ<br/>fetch キャッシュ]
C[コンテンツキャッシュ<br/>use cache]
D[フルルートキャッシュ<br/>全ルートキャッシュ]
end
A --> E[同一リクエスト内<br/>同じ fetch の重複排除]
B --> F[クロスリクエスト永続化<br/>force-cache/no-store]
C --> G[任意の関数結果のキャッシュ<br/>tag + life 制御]
D --> H[ページレベル HTML キャッシュ]
style C fill:#d4edda
| キャッシュレベル | スコープ | トリガー | ライフサイクル |
|---|---|---|---|
| リクエストメモ化 | 単一リクエスト | 自動 (同一 URL) | リクエスト終了 |
| データキャッシュ | クロスリクエスト | fetch(options) |
設定依存 |
| コンテンツキャッシュ | クロスリクエスト | 'use cache' ディレクティブ |
cacheLife + cacheTag |
| フルルートキャッシュ | クロスリクエスト | ビルド時 / ランタイム | Revalidate / オンデマンド |
(1) 基本構文
// app/cache-demo/actions.ts
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
export async function getExpensiveData(id: string) {
'use cache'
cacheTag('expensive', `id-${id}`) // タグ付け
cacheLife({ stale: 60, revalidate: 300 }) // 60秒以内は stale を返す、300秒で再検証
// この関数内のすべての操作がキャッシュされます
const result = await db.query(...)
return result
}
(2) cacheLife 設定
| パラメータ | 型 | 説明 | 例 |
|---|---|---|---|
stale |
number (秒) | キャッシュ有効期間中は結果を直接返す。バックグラウンド更新はトリガーしない | { stale: 60 } |
revalidate |
number (秒) | この時間経過後に関数を再実行する | { revalidate: 3600 } |
expire |
number (秒) | キャッシュが完全に期限切れ。強制再取得 | { expire: 86400 } |
▶ サンプル: 基本的な use cache の使用 (難易度 ⭐)
getExpensiveData コンポーネントの UI をレンダリングします。
// app/cache-basic/page.tsx
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
async function getServerTime() {
'use cache'
cacheTag('server-time')
cacheLife({ stale: 10, revalidate: 30 })
// 時間のかかる操作をシミュレート
await new Promise(resolve => setTimeout(resolve, 1000))
return { time: new Date().toISOString(), server: process.env.HOSTNAME ?? 'local' }
}
export default async function CacheBasicPage() {
const [t1, t2, t3] = await Promise.all([
getServerTime(),
getServerTime(),
getServerTime(),
])
return (
<div>
<h1>use cache — 基本</h1>
<p>呼び出し1: {t1.time}</p>
<p>呼び出し2: {t2.time}</p>
<p>呼び出し3: {t3.time}</p>
<p><em>3つの呼び出しすべてが同じキャッシュ結果を返しました (重複計算なし)</em></p>
</div>
)
}
呼び出し1: 2026-07-06T10:00:00.000Z
呼び出し2: 2026-07-06T10:00:00.000Z ← 同じタイムスタンプ (キャッシュヒット)
呼び出し3: 2026-07-06T10:00:00.000Z ← 同じタイムスタンプ (キャッシュヒット)
呼び出し1: 2026-07-06T10:00:00.000Z
呼び出し2: 2026-07-06T10:00:00.000Z ← キャッシュのミリ秒が重複
呼び出し3: 2026-07-06T10:00:00.000Z
4. cacheTag と cacheLife の実践的使用
cacheTag() はキャッシュコンテンツにラベルを付け、cacheLife() は有効期限ポリシーを制御します。revalidateTag() と組み合わせることで、きめ細かいキャッシュ制御が可能になります。
(1) タグ付けと分類戦略
graph TB
A[アプリケーションデータ] --> B[ユーザーデータ]
A --> C[商品データ]
A --> D[注文データ]
B --> E[tag: user-profile]
B --> F[tag: user-settings]
C --> G[tag: products]
C --> H[tag: product-{id}]
D --> I[tag: orders]
D --> J[tag: order-{id}]
style A fill:#cce5ff
| 戦略 | タグの例 | 無効化操作 | スコープ |
|---|---|---|---|
| きめ細かい | product-42 |
revalidateTag('product-42') |
単一商品のみ |
| 中粒度 | products |
revalidateTag('products') |
全商品 |
| 粗粒度 | catalog |
revalidateTag('catalog') |
ディレクトリ全体 |
(2) 動的タグ
// app/products/[id]/page.tsx
import { unstable_cacheTag as cacheTag } from 'next/cache'
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id)
return <ProductView product={product} />
}
async function getProduct(id: string) {
'use cache'
cacheTag('products', `product-${id}`) // パラメータに基づく動的タグ
return fetch(`https://api.example.com/products/${id}`).then(r => r.json())
}
▶ サンプル: 階層化キャッシュ戦略 (難易度: ⭐⭐)
データを取得し、結果をレンダリングします。
// app/cache-strategy/page.tsx
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
async function getUserProfile(userId: number) {
'use cache'
cacheTag('users', `user-${userId}`)
cacheLife({ stale: 120, revalidate: 600 }) // 2分 stale、10分 revalidate
return fetch(`https://jsonplaceholder.typicode.com/users/${userId}`).then(r => r.json())
}
async function getUserPosts(userId: number) {
'use cache'
cacheTag('posts', `user-posts-${userId}`)
cacheLife({ stale: 60, revalidate: 300 })
return fetch(`https://jsonplaceholder.typicode.com/users/${userId}/posts`).then(r => r.json())
}
export default async function CacheStrategyPage() {
const [profile, posts] = await Promise.all([
getUserProfile(1),
getUserPosts(1),
])
return (
<div>
<h1>{profile.name}</h1>
<p>Email: {profile.email}</p>
<h2>Posts ({posts.length})</h2>
<ul>{posts.map((p: any) => <li key={p.id}>{p.title}</li>)}</ul>
</div>
)
}
データをサーバーサイドで取得し、データからアイテムのリストをレンダリングします。
▶ サンプル: Server Action でタグによる無効化 (難易度: ⭐⭐)
コンポーネントがブラウザで説明された UI をレンダリングします。
// app/cache-invalidation/page.tsx
import { unstable_cacheTag as cacheTag, revalidateTag } from 'next/cache'
async function getTaskList() {
'use cache'
cacheTag('tasks')
return fetch('https://jsonplaceholder.typicode.com/todos?_limit=5').then(r => r.json())
}
export default async function CacheInvalidationPage() {
const tasks = await getTaskList()
return (
<div>
<h1>タスク一覧</h1>
<ul>{tasks.map((t: any) => <li key={t.id}>{t.title}</li>)}</ul>
<form action={async () => {
'use server'
revalidateTag('tasks') // ボタンクリックでタスク一覧のキャッシュを即時更新
}}>
<button type="submit">タスクを更新</button>
</form>
</div>
)
}
入力フィールドと送信ボタンを持つフォーム。
送信時、サーバーアクションがデータを処理し、関連するキャッシュタグを無効化します。
表示テキスト: Task List
5. 暗黙的な fetch キャッシュ vs 明示的な use cache キャッシュ
| 指標 | 暗黙的 fetch キャッシュ (データキャッシュ) | 明示的 use cache (コンテンツキャッシュ) |
|---|---|---|
| トリガー方法 | fetch(url) 自動 |
関数本体の 'use cache' ディレクティブ |
| キャッシュ内容 | HTTP レスポンス | 任意の関数が返す結果 |
| ユースケース | API 呼び出し、HTTP リクエスト | データベースクエリ、複雑な計算、ファイル読み取り |
| タグシステム | next: { tags: [...] } |
cacheTag() 関数 |
| 時間制御 | next: { revalidate: N } |
cacheLife() 関数 |
| 無効化方法 | revalidateTag() / revalidatePath() |
revalidateTag() (同じラベル) |
(1) いつ fetch キャッシュを使い、いつ use cache を使うべきか?
| シナリオ | 推奨キャッシュ方法 | 理由 |
|---|---|---|
| 外部 API の呼び出し | データキャッシュ (fetch) |
HTTP セマンティクスのネイティブ対応 |
| データベース呼び出し | コンテンツキャッシュ (use cache) |
データベース呼び出しは HTTP fetch ではない |
| 複雑な計算 (50 ms+) | コンテンツキャッシュ | 任意の関数をキャッシュ可能 |
| ファイル/設定の読み取り | コンテンツキャッシュ | ファイル操作は fetch で実行不可 |
| HTTP + DB 混合処理 | コンテンツキャッシュ | ロジック全体のブロックをキャッシュ |
▶ サンプル: 2つのキャッシュの組み合わせ (難易度: ⭐⭐⭐)
上記の説明通りにページをレンダリングし、説明の動作に基づいて UI が更新されます。
// app/cache-combo/page.tsx
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
// 1. fetch キャッシュ: 外部 API を呼び出し
async function getExternalPosts() {
return fetch('https://jsonplaceholder.typicode.com/posts', {
next: { tags: ['external-posts'], revalidate: 300 }
}).then(r => r.json())
}
// 2. use cache: 処理済みデータ
async function getProcessedPosts() {
'use cache'
cacheTag('processed-posts')
cacheLife({ stale: 60, revalidate: 600 })
const raw = await getExternalPosts() // fetch キャッシュに依存
return (raw as any[]).map((p: any) => ({
id: p.id,
title: p.title.toUpperCase(),
summary: p.body.slice(0, 100),
}))
}
export default async function CacheComboPage() {
const posts = await getProcessedPosts()
return (
<div>
<h1>Processed Posts ({posts.length})</h1>
<ul>{posts.map((p: any) => (
<li key={p.id}><strong>{p.title}</strong><p>{p.summary}</p></li>
))}</ul>
</div>
)
}
データをサーバーサイドで取得し、データからアイテムのリストをレンダリングします。
6. 完全な例: ユーザー管理システムのキャッシュアーキテクチャ
// app/cache-system/page.tsx — 完全なキャッシュアーキテクチャ
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
// ======== データレイヤー (use cache 明示的キャッシュ) ========
type User = { id: number; name: string; email: string }
type Post = { id: number; title: string; body: string }
async function getUsers(): Promise<User[]> {
'use cache'
cacheTag('users')
cacheLife({ stale: 120, revalidate: 600 })
return fetch('https://jsonplaceholder.typicode.com/users').then(r => r.json())
}
async function getUserPosts(userId: number): Promise<Post[]> {
'use cache'
cacheTag('posts', `user-posts-${userId}`)
cacheLife({ stale: 60, revalidate: 300 })
return fetch(`https://jsonplaceholder.typicode.com/users/${userId}/posts`).then(r => r.json())
}
async function getStats(users: User[]) {
'use cache'
cacheTag('stats')
cacheLife({ stale: 300, revalidate: 1800 })
return {
totalUsers: users.length,
avgNameLength: users.reduce((s, u) => s + u.name.length, 0) / users.length,
}
}
// ======== ページコンポーネント ========
export default async function CacheSystemPage() {
const users = await getUsers()
const stats = await getStats(users)
return (
<div style={{ maxWidth: 900, margin: '0 auto', padding: 24 }}>
<h1>ユーザー管理</h1>
<StatsCard stats={stats} />
<div style={{ display: 'grid', gap: 16, marginTop: 24 }}>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
</div>
)
}
// ======== 子コンポーネント (独立キャッシュ) ========
async function UserCard({ user }: { user: User }) {
const posts = await getUserPosts(user.id)
return (
<div style={{ border: '1px solid #ddd', borderRadius: 8, padding: 16 }}>
<h2>{user.name}</h2>
<p style={{ color: '#666' }}>{user.email}</p>
<details>
<summary>Posts ({posts.length})</summary>
<ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
</details>
</div>
)
}
function StatsCard({ stats }: { stats: { totalUsers: number; avgNameLength: number } }) {
return (
<div style={{ background: '#f0f4ff', borderRadius: 8, padding: 16, display: 'flex', gap: 32 }}>
<div><strong>合計ユーザー数</strong><p style={{ fontSize: 24 }}>{stats.totalUsers}</p></div>
<div><strong>平均名前長</strong><p style={{ fontSize: 24 }}>{stats.avgNameLength.toFixed(1)}</p></div>
</div>
)
}
// ======== 管理タスク ========
// app/cache-system/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function refreshUserData() {
revalidateTag('users') // ユーザー一覧を更新
}
export async function refreshPosts(userId: number) {
revalidateTag(`user-posts-${userId}`) // 特定ユーザーの投稿を更新
}
export async function refreshAll() {
revalidateTag('users')
revalidateTag('posts')
revalidateTag('stats')
}
// app/cache-system/admin-button.tsx
'use client'
export function AdminControls() {
return (
<div style={{ display: 'flex', gap: 8, margin: '16px 0' }}>
<form action={async () => {
const { refreshUserData } = await import('./actions')
await refreshUserData()
}}>
<button type="submit">ユーザーを更新</button>
</form>
<form action={async () => {
const { refreshAll } = await import('./actions')
await refreshAll()
}}>
<button type="submit">すべて更新</button>
</form>
</div>
)
}
▶ サンプル: キャッシュヒットの検証 (難易度: ⭐⭐)
入力フィールドと送信ボタンを持つフォーム。
送信時、サーバーアクションがデータを処理し、関連するキャッシュタグを無効化します。
表示テキスト: User Management
// app/cache-verify/page.tsx — キャッシュヒットを確認
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
let callCount = 0
async function getUniqueId() {
'use cache'
cacheTag('unique-id')
cacheLife({ revalidate: 30 })
callCount++
return { id: crypto.randomUUID(), calls: callCount }
}
export default async function CacheVerifyPage() {
const [a, b, c] = await Promise.all([
getUniqueId(),
getUniqueId(),
getUniqueId(),
])
return (
<div>
<h1>キャッシュ検証</h1>
<p>結果 A: {a.id} (呼び出し #{a.calls})</p>
<p>結果 B: {b.id} (呼び出し #{b.calls})</p>
<p>結果 C: {c.id} (呼び出し #{c.calls})</p>
<p><strong>同じ ID + calls = 1 → キャッシュヒット</strong></p>
</div>
)
}
内容: Cache Verification | Same ID + calls = 1 → キャッシュヒット
結果 A: 550e8400-e29b-41d4-a716-446655440000 (呼び出し #1)
結果 B: 550e8400-e29b-41d4-a716-446655440000 (呼び出し #1) ← キャッシュヒット
結果 C: 550e8400-e29b-41d4-a716-446655440000 (呼び出し #1) ← キャッシュヒット
❓ よくある質問
use cache と useMemo の違いは何ですか?useMemo はクライアントサイドのフックで、ブラウザ内でのみ計算をキャッシュし、単一レンダリングのスコープです。use cache はサーバーサイドのディレクティブで、リクエスト間でキャッシュを永続化し、タグと有効期限をサポートし、Server Actions でオンデマンド無効化できます。2つはまったく異なるユースケースです。cacheLife の「stale」と「revalidate」の違いは何ですか?stale 以内はキャッシュ結果が直接返されます (バックグラウンド更新はトリガーされません)。stale を超えても revalidate 未満の場合は、キャッシュ結果が返されますが、バックグラウンド再計算がトリガーされます。revalidate を超えると、新しい結果を待ちます。推奨設定: stale をユーザーが許容できる遅延に、revalidate をデータの最大鮮度に設定します。fetch と use cache の両方に使えますか?revalidateTag('products') は、fetch データキャッシュの next: { tags: ['products'] } ラベル付きエントリと、コンテンツキャッシュの cacheTag('products') ラベル付きエントリの両方をクリアします。これは統一キャッシュ無効化を実現するための重要なメカニズムです。use cache はクライアントコンポーネントで使用できますか?'use cache' ディレクティブはサーバーコンポーネントまたはサーバー関数でのみ有効です。クライアントコンポーネントでは、useMemo または React Query などのクライアントサイドキャッシュソリューションを使用する必要があります。unstable_cacheLife と unstable_cacheTag の unstable_ プレフィックスは何を意味しますか?unstable_ プレフィックスは通常 1〜2 のメジャーリリース後に削除されます (Next.js 17 または 18 の安定版で想定)。📖 まとめ
'use cache'ディレクティブは、HTTP fetch に限定されず、任意のサーバーサイド関数の結果をキャッシュ可能にしますcacheTag()はキャッシュコンテンツにタグを付けます。動的タグ (パラメータベース) をサポートしますcacheLife()はキャッシュ有効期限ポリシーを制御します (stale + revalidate のデュアルモード)revalidateTag()は fetch キャッシュとコンテンツキャッシュの対応するタグすべてを一度にクリアします- 暗黙的
fetchキャッシュ (データキャッシュ) は HTTP リクエストに適し、明示的use cache(コンテンツキャッシュ) は任意の関数に適します - キャッシュ粒度: 粗→細: ページレベル → データレベル → 関数レベル。
use cacheで関数レベルキャッシュを実装 - ベストプラクティス: データベースクエリ、複雑な計算、データ処理関数には
use cacheを、HTTP API にはfetchキャッシュを使用します
📝 練習問題
-
基本問題 (⭐):
app/cache-demo/page.tsxを作成し、'use cache'関数で模擬的な時間のかかる操作 (await new Promise(resolve => setTimeout(resolve, 2000))) をラップします。ページ上で3回呼び出し、2回目以降の呼び出しがゼロ遅延 (キャッシュヒット) で開始されることを確認します。 -
応用問題 (⭐⭐): ユーザー一覧とユーザー記事一覧を含むページを構築します。ユーザー一覧には
cacheLife({ revalidate: 300 })を、記事一覧にはcacheLife({ revalidate: 60 })を使用します。ページ下部に、ユーザータブと記事タブをそれぞれ更新する2つの Server Action ボタンを追加します。タブが独立して機能することを確認します。 -
発展問題 (⭐⭐⭐): 3つの異なるソース (HTTP API + 模擬データベース + 計算関数) からデータを取得し、
use cacheで集中キャッシュする「データ分析ダッシュボード」を実装します。各データソースに異なるcacheLife戦略を設定します。revalidateTagを呼び出してすべてのダッシュボードを同時に更新する「強制更新」ボタンを追加します。キャッシュヒット率を追跡するツールを構築します。