はじめに
Next.js 16でキャッシュ周りが変わったらしいが全然追えていなかったので今回はuse cacheに関して整理してみました。
キャッシュは1箇所じゃない
前提知識としてWebアプリケーションの「キャッシュ」は1つの場所だけに存在するものではありません。
Browser
⇩
CDN / Edge
⇩
Application Server
⇩
Database / API
上記のようにブラウザにはHTTPのBrowser Cacheがあり、CloudflareのようなCDNにはEdge Cacheがあります。そしてNext.js自身にもデータの再利用やレンダリング結果の再利用を行う仕組みがあります。
Cache Componentsとは何か
Cache Componentsでは、データ取得を含む動的な処理はデフォルトではキャッシュされません。キャッシュしたい場合は、ページやコンポーネント、関数の先頭に明示的にuse cacheを指定します。
ただし、ネットワークアクセスやリクエスト時のデータを必要としない部分は自動的にprerenderされ、static shellに含まれます。データ取得などを含む動的な処理については、use cacheでキャッシュするのか、Suspenseを使ってリクエスト時にレンダリングするのかを明示的に選ぶ、というイメージです。
利用するにはnext.config.tsでcacheComponents: trueを設定します。
import type { NextConfig } from "next"
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfiguse cacheの基本
use cacheはファイル・関数・コンポーネントの先頭に書くディレクティブです。付ける場所によってキャッシュされる範囲が変わります。
①関数の先頭に書く
その関数の戻り値だけがキャッシュされます。
async function getProducts() {
"use cache"
const res = await fetch("https://api.example.com/products")
return res.json()
}
②コンポーネントの先頭に書く
そのコンポーネントのレンダリング結果がキャッシュされます。
async function ProductList() {
"use cache"
const products = await getProducts()
return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
)
}
③ファイルの先頭に書く
そのファイルからexportされるすべての関数がキャッシュ対象になる
※ ファイルレベルで指定する場合、exportされる関数はasync functionである必要があります。
cacheLife / cacheTag
特に指定をしない場合、キャッシュのライフサイクルはデフォルト値になりますが、cacheLifeとcacheTagを組み合わせることで寿命と無効化のタイミングを制御することができます。
import { cacheLife, cacheTag } from "next/cache"
async function getProducts() {
"use cache"
cacheLife("hours")
cacheTag("products")
const res = await fetch("https://api.example.com/products")
return res.json()
}
・cacheLife(profile): "seconds" / "minutes" / "hours" / "days" / "weeks" / "max" のようなプロファイル名、もしくは独自の設定でキャッシュの寿命を指定する
・cacheTag(tag): キャッシュにタグを付けておき、revalidateTag(tag, "max")呼び出すことで対象をstaleにして再検証できる
上記の例では「商品情報を更新した直後に反映したい」というようなケースでは更新処理の中でrevalidateTag("products", "max")を呼べば、対象をstaleとして扱い、次のアクセス時にstale-while-revalidateで再検証されます。更新直後から最新データを必要とする場合にはupdateTag()も利用できます。
キャッシュしない動的な処理とSuspense
cookies()やheaders()、searchParamsなどのリクエスト時にしか取得できない値や、キャッシュしていないfetchなどをコンポーネントの中で使う場合は、その部分を<Suspense>で囲む必要があります。
import { Suspense } from "react"
import { cookies } from "next/headers"
async function UserGreeting() {
const cookieStore = await cookies()
const name = cookieStore.get("username")?.value ?? "ゲスト"
return <p>こんにちは、{name}さん</p>
}
export default function Page() {
return (
<div>
{/* prerender可能な部分 + use cacheの結果がstatic shellに含まれる */}
<ProductList />
<Suspense fallback={<p>読み込み中...</p>}>
<UserGreeting /> {/* 動的:Suspense内でストリーミングされる */}
</Suspense>
</div>
)
}
Cache Componentsでは、prerender可能な部分やuse cacheでキャッシュした部分をstatic shellに含めつつ、リクエスト時のデータを必要とする部分はSuspense境界の内側で動的にレンダリングできます。これにより、1つのルートの中に静的な部分と動的な部分を共存させることができます。
どういうデータをキャッシュすると良いか
結局どこでuse cacheを使えばいいのか?悩むので以下で判断基準を整理してみました。
| データの性質 | 例 | 方針 |
| --- | --- | --- |
| 更新頻度が低い | 商品マスタ・記事・カテゴリ一覧 | `use cache`と相性がいい |
| ユーザーによって内容が変わる | ユーザーごとのプロフィール | 安易に共有キャッシュしない |
| 常に最新性が重要 | 現在時刻・リアルタイム在庫 | キャッシュしない、または短い`cacheLife` |
| 多少古くても問題ない | 記事一覧 | `cacheLife`を設定してキャッシュ |
ポイントは「ユーザーによって結果が変わるかどうか」と「多少古くても許容できるかどうか」の2軸で考えることだと思います。ユーザー固有データをキャッシュする場合は注意が必要です。cookies()やheaders()などのRuntime APIはuse cacheのスコープ内から直接利用できないため、外側で値を取得して引数として渡します。その引数はキャッシュキーの一部になるため、ユーザーごとに異なるキャッシュエントリとして扱えます。ただし、ユーザーごとにキャッシュが細分化されるため、本当にキャッシュするメリットがあるかは検討する必要があります。
まとめ
- Next.js 16のCache Componentsでは、静的にprerenderできる部分は自動的にstatic shellへ含めつつ、キャッシュしたいページ・コンポーネント・関数にはuse cache を付け、リクエスト時のデータが必要な部分はSuspenseでレンダリングする
- use cacheをファイル・関数・コンポーネントの先頭に付けることでキャッシュ範囲を指定する
- cacheLifeでstale・revalidate・expireをまとめたライフサイクルを、cacheTag + revalidateTag(tag, "max")でオンデマンド再検証を制御する。更新直後から最新が必要な場合はupdateTag()
- cookies()やheaders()、searchParamsなどのリクエスト時にしか取得できない値や、キャッシュしていないデータ取得は<Suspense>で囲み、static shellと切り分ける
- キャッシュすべきかどうかは「ユーザーによって結果が変わるか」「多少古くても許容できるか」で判断する
Browser CacheやCDN Cache、Client-side Router Cacheについては、また別の機会に整理したいと思います。