Server Componentsでの直接データフェッチ

Next.js App RouterのServer Componentsでは、コンポーネント内で直接async/awaitを使ってデータを取得できます。 Pages Router時代のgetServerSidePropsgetStaticPropsのような特殊な関数は不要になり、 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/>メモ化キャッシュは破棄
Request Memoization: 同一リクエスト内で3つのコンポーネントが同じfetchを呼び出しても、実際のAPIコールは1回のみ

この仕組みにより、コンポーネントツリーの複数箇所で同じデータが必要な場合でも、 「データを親から受け取るために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: 最新のHTML
ISRのStale-While-Revalidateフロー: staleなページを即座に返しつつ、バックグラウンドで再生成する

Runtime 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)といった レンダリング戦略をどのように使い分けるかを解説します。

理解度チェック

問題 0 / 50%
Q1

Request Memoizationの有効範囲として正しいのはどれですか?

キーボード: 1〜4 で選択、Enter で回答