7.

Next.js の use cache|Cache Components の基礎

編集

この記事の要点

  • バージョン 16 のキャッシュは Cache Components に整理された。next.config.tscacheComponents: true を書いて有効化する。
  • キャッシュしたい関数やコンポーネントの本体先頭に 'use cache' と書く。オプションではなくコード側で宣言する。
  • 引数とスコープ外から参照している値がキャッシュキーになる。引数が違えば別のエントリになる。
  • キャッシュしないデータや cookies() などは Suspense で包む。これが「静的な殻+動的な中身」を両立させる形になる。
  • Math.random()Date.now() は明示的な扱いが必要。リクエストごとに変えるなら connection() を呼ぶ。

Next.js のキャッシュは歴史的に何度も設計が変わってきた領域で、日本語の解説が最も食い違いやすい部分でもあります。バージョン 13 では fetch が既定でキャッシュされ、export const revalidate のようなルート単位の設定が主役でした。バージョン 16 では考え方が変わり、キャッシュするものをコード側で宣言する方式になっています。この記事ではその新しいモデルを扱います。

1有効化する

Cache Components は設定で有効にします。

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig

これは名前替えではありません。有効にすると、キャッシュも Suspense も無いままリクエスト時のデータへアクセスしている箇所がビルドエラーとして表面化します。既存プロジェクトに後から入れる場合は、その修正込みで計画してください。

2use cache の 2 つの粒度

'use cache' は関数本体の先頭に書きます。適用の仕方は 2 段階あります。

データ単位。取得や計算を行う関数をキャッシュします。同じデータを複数のコンポーネントで使う場合や、UI と切り離して寿命を管理したい場合に向きます。

// app/lib/data.ts
import { cacheLife } from 'next/cache'

export async function getUsers() {
  'use cache'
  cacheLife('hours')
  return db.query('SELECT * FROM users')
}

UI 単位。コンポーネントやページ全体をキャッシュします。描画結果ごと保存されるため、取得と整形の両方を省けます。

// app/page.tsx
import { cacheLife } from 'next/cache'

export default async function Page() {
  'use cache'
  cacheLife('hours')

  const users = await db.query('SELECT * FROM users')

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

ファイルの先頭に書けば、そのファイルの全エクスポートがキャッシュ対象になります。また、cacheLife を必ず添えることが推奨されています。省略すると既定のプロファイルが適用されますが、意図が読めなくなるためです。

3キャッシュキーは引数で決まる

キャッシュの区別は引数と、関数が外側のスコープから参照している値で行われます。

async function getProduct(id: string) {
  'use cache'
  cacheLife('hours')
  return db.products.find(id)
}
// getProduct('a') と getProduct('b') は別々に保存される

したがって、ユーザーごとに変わる値を引数として渡せば、ユーザーごとのキャッシュを作れます。逆に言うと、キーになりうる値を引数に取らずに関数の中で直接読んでしまうと、全員が同じ結果を共有してしまいます。

4キャッシュしないものは Suspense で包む

毎回新鮮でなければならないデータには 'use cache' を付けません。代わりに Suspense で包みます。

import { Suspense } from 'react'

async function LatestPosts() {
  const data = await fetch('https://api.example.com/posts')
  const posts = await data.json()
  return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
}

export default function Page() {
  return (
    <>
      <h1>ブログ</h1>
      <Suspense fallback={<p>読み込み中…</p>}>
        <LatestPosts />
      </Suspense>
    </>
  )
}

cookies()headers()searchParamsparams といったリクエスト依存の値も同じ扱いです。これらを Suspense なしで読むと、開発時のオーバーレイに「ルート全体をブロックしている」という指摘が出ます。

1 つのページの中での役割分担 静的な部分 見出し・ナビゲーション・固定文言 → ビルド時に確定し、静的な殻に入る 'use cache' を付けた部分 記事一覧・商品マスタなど、全員に同じものを見せてよいデータ → 保存された結果が静的な殻に同梱され、cacheLife の期限で作り直される Suspense で包んだ部分 ログイン名・カート・在庫など、リクエストごとに変わるデータ → フォールバックが先に届き、中身は後から流れてくる
静的・キャッシュ済み・リクエスト時の 3 層を 1 ページに同居させる。これが Cache Components を有効にしたときの既定の描画モデル(Partial Prerendering)。

5リクエスト依存の値をキャッシュに渡す

「セッションごとに違うが、同じセッションなら使い回せる」データもあります。この場合は、リクエスト依存の値をいったん取り出してから、引数としてキャッシュ関数に渡します

import { cookies } from 'next/headers'
import { Suspense } from 'react'

export default function Page() {
  return (
    <Suspense fallback={<div>読み込み中…</div>}>
      <ProfileContent />
    </Suspense>
  )
}

// キャッシュしない側でリクエスト値を読む
async function ProfileContent() {
  const session = (await cookies()).get('session')?.value
  return <CachedContent sessionId={session} />
}

// 受け取った値がキャッシュキーになる
async function CachedContent({ sessionId }: { sessionId: string }) {
  'use cache'
  const data = await fetchUserData(sessionId)
  return <div>{data}</div>
}

なお、既定のキャッシュはインスタンスごとのメモリ上に置かれます。サーバーレス環境のようにインスタンスが使い捨てられる構成では長持ちしません。永続的に共有したい場合は 'use cache: remote' を使い、外部ストアを設定します。逆に、ブラウザ側にだけ保持したい場合は 'use cache: private' という選択肢もあります。

6ランダム値と現在時刻

Math.random()Date.now()crypto.randomUUID() は実行のたびに違う値になるため、事前生成と相性が悪く、明示的な指定が必要です。

リクエストごとに変えたい
connection() を先に await してから生成し、そのコンポーネントを Suspense で包む。
全員で同じ値を共有してよい
'use cache' を付ける。再検証されるまで同じ値が返る。
import { connection } from 'next/server'
import { Suspense } from 'react'

async function UniqueContent() {
  await connection()               // ここから先はリクエスト時に実行される
  const uuid = crypto.randomUUID()
  return <p>リクエスト ID: {uuid}</p>
}

export default function Page() {
  return (
    <Suspense fallback={<p>読み込み中…</p>}>
      <UniqueContent />
    </Suspense>
  )
}

一方、モジュールの読み込みや同期的なファイル読み取り、純粋な計算は毎回同じ結果になるため、そのまま事前生成に含まれます。設定ファイルのように「リクエストに依存せず変化もしない」ものは、コンポーネントの中ではなくモジュールのトップレベルで一度読むのが最も簡潔です。

7動的な URL と ISR

[slug] のような動的セグメントでは、generateStaticParams に列挙した URL がビルド時に生成されます。列挙していない URL に対しては、まず URL 非依存の共通部分(アプリシェル)が即座に返され、背後で具体的な内容が生成されて次の訪問者から使われます。これがバージョン 16 における ISR の姿です。

8つまずきやすいところ

  • 古い記事の設定が効かないexperimental.pprexperimental.dynamicIOexperimental.useCache はいずれも削除されました。cacheComponents に置き換えます。
  • fetch が勝手にキャッシュされると思っている:バージョン 16 ではされません。キャッシュしたいなら 'use cache' を明示します。
  • デプロイしたらキャッシュが消えた:キャッシュキーにビルド ID が含まれるため、新しいデプロイでは必ず作り直しになります。外部ストアを使っていても同じです。
  • 個人情報が共有されてしまった:リクエスト依存の値をキャッシュ関数の外から読んでいないか確認します。キーに含まれない値を内部で読むのが事故のもとです。

キャッシュを置いたら、次はそれをいつ捨てるかという設計に進みます。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 開発環境の構築とプロジェクトの初期構成
  2. App Router のルーティングとファイル規約
  3. レイアウトと特殊ファイルの使い分け
  4. サーバーコンポーネントとクライアントコンポーネント
  5. サーバー側でのデータ取得と並列化
  6. ストリーミングで体感速度を上げる
  7. キャッシュの仕組みと use cache
  8. 再検証の使い分け
  9. フォーム送信とデータ更新の実装
  10. エラーハンドリングとエラー境界
  11. Route Handlers で API を作る
  12. メタデータ API で SEO を整える
  13. 画像とフォントの最適化
  14. 本番ビルドとセルフホスト
  15. バージョン16 への移行と注意点