Server Componentsでの直接データフェッチ
Next.js App RouterのServer Componentsでは、コンポーネント内で直接async/awaitを使ってデータを取得できます。
Pages Router時代のgetServerSidePropsやgetStaticPropsのような特殊な関数は不要になり、
Reactコンポーネントが自然にデータフェッチの責務を持てるようになりました。
// app/posts/page.tsx — Server Componentでの直接fetch
export default async function PostsPage() {
// サーバー側で直接データを取得(クライアントにバンドルされない)
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return (
<ul>
{posts.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
} この方式の最大のメリットはコロケーションです。 データの取得とその表示が同じコンポーネント内に配置されるため、コードの見通しが格段に良くなります。 また、Server Component内のfetchはサーバー側で実行されるため、APIキーやデータベース接続情報がクライアントに漏洩することがありません。
Request Memoization — 同一リクエスト内の自動メモ化
Next.jsは、同一のサーバーレンダリングリクエスト内で同じURLとオプションを持つfetch()呼び出しを自動的にメモ化します。
これをRequest Memoizationと呼びます。
sequenceDiagram
participant C1 as コンポーネントA
participant C2 as コンポーネントB
participant C3 as コンポーネントC
participant Cache as Request<br/>Memoization
participant API as 外部API
C1->>Cache: fetch('/api/user')
Cache->>API: GET /api/user(初回)
API-->>Cache: レスポンス
Cache-->>C1: データ返却
C2->>Cache: fetch('/api/user')
Cache-->>C2: キャッシュから返却(APIコールなし)
C3->>Cache: fetch('/api/user')
Cache-->>C3: キャッシュから返却(APIコールなし)
Note over Cache: リクエスト完了後に<br/>メモ化キャッシュは破棄この仕組みにより、コンポーネントツリーの複数箇所で同じデータが必要な場合でも、 「データを親から受け取るためにpropsをバケツリレーする」必要がなくなります。 各コンポーネントが独立して必要なデータをfetchすればよく、重複リクエストはフレームワークが自動的に排除します。
// React.cache()を使った手動メモ化(fetch以外のデータソース向け)
import { cache } from 'react';
import { db } from '@/lib/db';
// 同一リクエスト内で複数回呼ばれても、DBクエリは1回だけ実行される
export const getUser = cache(async (id: string) => {
return await db.user.findUnique({ where: { id } });
}); 'use cache'ディレクティブ — Cache Componentsの詳細
Next.js v15で実験的に導入され、v16で安定化した'use cache'ディレクティブは、
キャッシュの制御をReactのコンポーネントモデルに統合する画期的な仕組みです。
'use client'がクライアント境界を宣言するように、'use cache'はキャッシュ境界を宣言します。
3つの適用レベル
'use cache'はファイルレベル、コンポーネントレベル、関数レベルの3段階で適用できます。
// 1. ファイルレベル — ファイル内のすべてのエクスポートをキャッシュ
'use cache';
export async function getProducts() {
const res = await fetch('https://api.example.com/products');
return res.json();
}
export async function getCategories() {
const res = await fetch('https://api.example.com/categories');
return res.json();
}
// 2. コンポーネントレベル — 特定のコンポーネントの出力をキャッシュ
export async function ProductCard({ id }: { id: string }) {
'use cache';
const product = await fetch('https://api.example.com/products/' + id);
const data = await product.json();
return (
<div>
<h3>{data.name}</h3>
<p>{data.price}円</p>
</div>
);
}
// 3. 関数レベル — 特定のデータフェッチ関数をキャッシュ
export async function getProductById(id: string) {
'use cache';
const res = await fetch('https://api.example.com/products/' + id);
return res.json();
} キャッシュキー生成アルゴリズム
'use cache'のキャッシュキーは、以下の4要素から自動的に生成されます。
開発者がキーを手動で管理する必要はありません。
| 要素 | 説明 | 例 |
|---|---|---|
| Build ID | ビルドごとに一意のID。新しいデプロイでキャッシュが自動的に無効化される | abc123 |
| Function ID | 関数のソースコード上の位置から生成されるID | app/products/page.tsx#getProducts |
| 引数(Arguments) | シリアライズ可能な関数の引数 | { id: "prod-001" } |
| クロージャ変数 | 関数が参照するスコープ外の変数(シリアライズ可能なもの) | categoryId = "electronics" |
cacheLife() — キャッシュの有効期間プロファイル
cacheLife()関数でキャッシュの有効期間を制御します。
Next.jsには5つの組み込みプロファイルが用意されています。
| プロファイル | stale(秒) | revalidate(秒) | expire(秒) | 用途 |
|---|---|---|---|---|
default | 300(5分) | 900(15分) | 4,294,967,294(≒136年) | 頻繁な更新が不要なデフォルト |
seconds | 0 | 1 | 60 | ほぼリアルタイムのデータ |
minutes | 300(5分) | 60 | 3,600(1時間) | 1時間以内に更新されるデータ |
hours | 300(5分) | 3,600(1時間) | 86,400(1日) | 日次更新のデータ |
days | 300(5分) | 86,400(1日) | 604,800(1週間) | 週次更新のデータ |
weeks | 300(5分) | 604,800(1週間) | 2,592,000(30日) | 月次更新のデータ |
max | 300(5分) | 2,592,000(30日) | 4,294,967,294(≒136年) | ほぼ不変のデータ |
import { cacheLife } from 'next/cache';
export async function getPopularProducts() {
'use cache';
cacheLife('hours'); // 1時間ごとに再検証、最大1日保持
const res = await fetch('https://api.example.com/popular');
return res.json();
}
// カスタムプロファイルも定義可能(next.config.ts)
// next.config.ts
const nextConfig = {
experimental: {
cacheLife: {
'blog-posts': {
stale: 600, // 10分間はstaleデータを返す
revalidate: 3600, // 1時間ごとにバックグラウンド再検証
expire: 86400, // 24時間で完全に期限切れ
},
},
},
};
export default nextConfig; cacheTag()とrevalidateTag() — タグベースの無効化
cacheTag()でキャッシュにタグを付与し、revalidateTag()でタグに紐づくすべてのキャッシュを一括無効化できます。
これにより、CMSのWebhookやServer Actionからのオンデマンド再検証が実現します。
import { cacheTag, cacheLife } from 'next/cache';
import { revalidateTag } from 'next/cache';
// データフェッチ側: タグを付与
export async function getPost(slug: string) {
'use cache';
cacheTag('posts', 'post-' + slug);
cacheLife('days');
const res = await fetch('https://cms.example.com/posts/' + slug);
return res.json();
}
// Server Action側: タグで無効化
export async function publishPost(slug: string) {
'use server';
// DBに保存した後、該当記事のキャッシュを無効化
await db.post.update({ where: { slug }, data: { published: true } });
revalidateTag('post-' + slug); // 特定記事のキャッシュを無効化
revalidateTag('posts'); // 記事一覧のキャッシュも無効化
} キャッシュスコープ — remote と private
'use cache'には3つのスコープがあり、キャッシュの共有範囲を制御できます。
'use cache: remote' — リモートキャッシュ
'use cache: remote'は、CDNやエッジネットワーク上の共有キャッシュにデータを保存します。
複数のサーバーインスタンス間でキャッシュを共有でき、スケーラブルなアプリケーションに適しています。
Vercel環境ではグローバルなエッジキャッシュが自動的に使用されます。
// リモートキャッシュ: 全サーバーインスタンスで共有
export async function getGlobalConfig() {
'use cache: remote';
cacheLife('days');
const res = await fetch('https://api.example.com/config');
return res.json();
} 'use cache: private' — プライベートキャッシュ
'use cache: private'は、ユーザー固有のデータをキャッシュするためのスコープです。
通常の'use cache'では使用できないcookies()へのアクセスが可能で、
認証済みユーザーのダッシュボードデータなど、パーソナライズされたコンテンツのキャッシュに適しています。
import { cookies } from 'next/headers';
// プライベートキャッシュ: ユーザーごとにキャッシュが分離
export async function getUserDashboard() {
'use cache: private';
cacheLife('minutes');
const cookieStore = await cookies();
const sessionId = cookieStore.get('session')?.value;
const res = await fetch(
'https://api.example.com/dashboard?session=' + sessionId
);
return res.json();
} | スコープ | 共有範囲 | cookies()アクセス | 主な用途 |
|---|---|---|---|
'use cache'(デフォルト) | サーバーローカル | 不可 | 汎用的なデータキャッシュ |
'use cache: remote' | グローバル(CDN/エッジ) | 不可 | 全ユーザー共通のデータ |
'use cache: private' | ユーザーごとに分離 | 可能 | パーソナライズデータ |
ISR — revalidateによる時間ベース再検証
ISR(Incremental Static Regeneration)は、静的に生成されたページをバックグラウンドで段階的に再生成する仕組みです。
App Routerでは、Route Segmentの設定としてrevalidate値を指定することで有効になります。
// app/blog/[slug]/page.tsx — ISRの設定
export const revalidate = 3600; // 1時間ごとに再検証
export async function generateStaticParams() {
const posts = await fetch('https://cms.example.com/posts')
.then(r => r.json());
return posts.map((post: { slug: string }) => ({
slug: post.slug,
}));
}
export default async function BlogPost(
{ params }: { params: Promise<{ slug: string }> }
) {
const { slug } = await params;
const post = await fetch(
'https://cms.example.com/posts/' + slug
).then(r => r.json());
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
} sequenceDiagram
participant U1 as ユーザーA
participant U2 as ユーザーB
participant CDN as CDN/サーバー
participant Build as バックグラウンド再生成
Note over CDN: ビルド時に静的HTML生成済み<br/>revalidate = 3600(1時間)
U1->>CDN: GET /blog/hello(0分経過)
CDN-->>U1: キャッシュ済みHTML(即座に返却)
Note over CDN: 61分経過...stale状態
U2->>CDN: GET /blog/hello(61分経過)
CDN-->>U2: staleなHTMLを即座に返却
CDN->>Build: バックグラウンドで再生成開始
Build-->>CDN: 新しいHTMLで置き換え
U2->>CDN: GET /blog/hello(次のリクエスト)
CDN-->>U2: 最新のHTMLRuntime APIの制約
'use cache'内では、リクエストごとに異なる値を返すRuntime API(動的API)の使用が制限されます。
これは、キャッシュされた結果が複数のリクエストで再利用されるため、リクエスト固有の情報に依存できないからです。
| API | 'use cache'内 | 'use cache: private'内 | 通常のServer Component |
|---|---|---|---|
cookies() | 使用不可 | 使用可能 | 使用可能 |
headers() | 使用不可 | 使用不可 | 使用可能 |
searchParams | 引数として渡す | 引数として渡す | 直接アクセス可能 |
connection() | 使用不可 | 使用不可 | 使用可能 |
Date.now() | キャッシュ時点の値が固定 | キャッシュ時点の値が固定 | リクエスト時の値 |
旧モデルからの移行 — unstable_cacheからuse cacheへ
Next.js v14で導入されたunstable_cacheは、'use cache'の前身にあたるAPIです。
v16では'use cache'への移行が推奨されており、将来的にunstable_cacheは非推奨になる予定です。
| 機能 | unstable_cache(旧) | 'use cache'(新) |
|---|---|---|
| キャッシュキー | 手動で文字列キーを指定 | 引数・クロージャから自動生成 |
| 有効期間 | revalidateオプションのみ | cacheLife()で詳細制御 |
| タグ付け | tagsオプション | cacheTag()関数 |
| 適用対象 | 関数のみ | ファイル・コンポーネント・関数 |
| スコープ | なし | remote / private対応 |
| 型安全性 | 部分的 | 完全(引数の型がそのまま利用) |
// Before: unstable_cache(非推奨へ移行予定)
import { unstable_cache } from 'next/cache';
const getCachedPosts = unstable_cache(
async () => {
return await db.post.findMany();
},
['posts'], // 手動キー指定
{ revalidate: 3600, tags: ['posts'] }
);
// After: 'use cache'(推奨)
import { cacheLife, cacheTag } from 'next/cache';
async function getCachedPosts() {
'use cache';
cacheLife('hours');
cacheTag('posts');
return await db.post.findMany();
} まとめ — キャッシュ戦略の選択指針
Next.jsのデータフェッチとキャッシュ戦略は、「デフォルトでキャッシュしない」という原則のもと、
開発者が明示的にキャッシュを選択する設計になっています。
Request Memoizationは自動的に適用されますが、リクエストをまたぐキャッシュは'use cache'で明示的に宣言する必要があります。
'use cache'ディレクティブの導入により、キャッシュの制御がReactのコンポーネントモデルと自然に統合されました。
ファイル・コンポーネント・関数の3レベルでの適用、cacheLife()による有効期間の宣言的制御、
cacheTag()によるオンデマンド無効化は、従来のunstable_cacheと比べて大幅に洗練されたAPIです。
次章では、これらのキャッシュ戦略を踏まえた上で、SSG、SSR、Streaming、PPR(Partial Prerendering)といった レンダリング戦略をどのように使い分けるかを解説します。
理解度チェック
Request Memoizationの有効範囲として正しいのはどれですか?
キーボード: 1〜4 で選択、Enter で回答