- Next.js
Next.js use cacheの使い方|Cache Components実測ガイド
"use cache" とは、Next.js 16 の Cache Components で「この関数・コンポーネント・ファイルの結果をキャッシュする」と宣言するためのディレクティブ(ファイルや関数の先頭に書く指示文)です。 next.config.ts で cacheComponents: true にすると、何も書かない部分はリクエストのたびに実行され、"use cache" を書いた部分だけがキャッシュされます。
この記事では、Next.js 16.3.6 で実際にアプリを作り、"use cache" の3つの置き場所、cacheLife のプロファイル一覧、cacheTag と4つの無効化関数(updateTag・revalidateTag・refresh・revalidatePath)の違いをPlaywright で画面の件数・時刻を読み取って確かめました。<Suspense> なしで未キャッシュのデータを読んだときのビルドエラーの実文と直し方3パターン、ビルド出力の ○ ◐ ƒ の読み方も、実際の出力つきで載せています。
use cache / Cache Components の早見表
| やりたいこと | 書き方 |
|---|---|
| Cache Components を有効にする | next.config.ts に cacheComponents: true |
| 関数の結果をキャッシュ | 関数の先頭に "use cache"(async 必須) |
| コンポーネントの出力をキャッシュ | コンポーネントの先頭に "use cache" |
| ファイル内の export をすべてキャッシュ | ファイルの1行目に "use cache" |
| 有効期間を決める | cacheLife("hours") などのプロファイル、またはオブジェクト |
| 独自の期間に名前を付ける | next.config.ts の cacheLife: { 名前: {...} } |
| あとから狙って捨てられるようにする | cacheTag("products") |
| 変更直後に自分の画面へ反映(Server Action) | updateTag("products") |
| 古い内容を返しつつ裏で作り直す | revalidateTag("products", "max") |
| キャッシュしていない部分だけ取り直す | refresh()(Server Action) |
| パス単位で捨てる | revalidatePath("/products") |
| キャッシュしないデータを流す | <Suspense> で囲む(await connection() でリクエスト時に確定) |
| cookie の値を使いたい | キャッシュの外で読み、引数で渡す |
| 待つページだと明示する | export const instant = false |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Next.js use cacheの使い方|Cache Components実測ガイド」
検証環境とバージョン
| 項目 | 内容 |
|---|---|
| Next.js | 16.3.6(npm latest、2026-09-22)。9月30日にセキュリティリリース予告があるため、公開後は 16.3.7 以降を推奨 |
| React | 19.2.8(create-next-app@16.3.6 の既定) |
| Node.js | 22.22.2(Linux、2 vCPU) |
| 確認方法 | next build → next start → Playwright(Chromium)で表示内容を読み取り |
"use cache" は 15.0 で実験的に登場し、16.0 から Cache Components の機能として正式に使えるようになりました。15 時代の記事にある experimental.dynamicIO や experimental.useCache は、16 では cacheComponents にまとめられています。
Cache Components を有効にすると何が変わるか
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
cacheLife: {
// 独自プロファイル:5分ごとに裏で作り直し、1日で必ず期限切れ
catalog: { stale: 60, revalidate: 300, expire: 86400 },
},
};
export default nextConfig;
ビルドすると - Cache Components enabled と表示されます。有効にすると、ページの中の「データを待つ部分」ごとに、次の3つのどれにするかを決める考え方になります。
flowchart TD
A[コンポーネントがデータを待つ] --> B{毎回同じ結果でよい?}
B -- はい --> C["'use cache' + cacheLife<br/>→ 静的シェルに入る"]
B -- いいえ --> D{読み込み中の表示を出せる?}
D -- はい --> E["Suspense で囲む<br/>→ あとから流し込む"]
D -- いいえ --> F["export const instant = false<br/>→ そろうまで待つ"]
- 静的シェル:ビルド時に作っておく HTML の骨組み。最初に一瞬で表示されます。
- 部分プリレンダー(PPR):静的シェルを先に返し、
<Suspense>の中身をあとからストリーミングする描画方法です。
"use cache" を置ける3つの場所
関数単位:いちばんよく使う形
// lib/products.ts
import { cacheLife, cacheTag } from "next/cache";
import { readProducts } from "./db";
// 関数単位のキャッシュ:タグ 'products' をつけて、あとから狙って捨てられるようにする
export async function getProducts() {
"use cache";
cacheTag("products");
cacheLife("catalog"); // next.config.ts で定義した独自プロファイル
const items = await readProducts();
return { items, generatedAt: new Date().toISOString().slice(11, 19) };
}
ファイル単位:export したすべての関数がキャッシュされる
// lib/categories.ts
// ファイルの先頭に書くと、export したすべての async 関数がキャッシュされる
"use cache";
import { cacheLife } from "next/cache";
export async function getCategories() {
cacheLife("days");
return ["文具", "備品", "消耗品"];
}
コンポーネント単位:出力の JSX ごとキャッシュ
async のコンポーネントの先頭に "use cache" を書きます。後半の時刻ページの BuildTime がこの形です。props がキャッシュのキーになります。
| 置き場所 | キャッシュの単位 | キーに入るもの | 向いている場面 |
|---|---|---|---|
| 関数 | 戻り値 | 引数+外側から参照した変数 | DB・API の読み取り |
| コンポーネント | 出力(JSX) | props | 重い集計を含む表示部品 |
| ファイル | export した各関数 | 各関数の引数 | 読み取り専用の関数をまとめたモジュール |
キャッシュのキーは、ビルドID・関数の場所・引数(props)から自動で作られます。 外側のスコープの変数を参照すると、それも自動でキーに入ります。
引数と戻り値はシリアライズ(送れる形に変換)できる値に限られます。 文字列・数値・プレーンなオブジェクト・配列・Date・Map・Set は使えます。クラスのインスタンス・関数・Symbol・URL インスタンスは使えません。children のような JSX は、中身を読まずに「そのまま通す」なら受け取れます。
cacheLife プロファイル一覧
cacheLife は "use cache" と同じスコープの中で呼び、キャッシュの有効期間を決めます。 3つの値の意味は次のとおりです。
| 値 | どこで効くか | 意味 |
|---|---|---|
stale |
ブラウザー(クライアントのルーター) | この時間はサーバーに確認せずに手元のキャッシュを使う(最低30秒) |
revalidate |
サーバー | この時間を過ぎた次のリクエストで、古い内容を返しつつ裏で作り直す |
expire |
サーバー | アクセスがないままこの時間を過ぎたら、次のリクエストは新しい内容を待つ |
組み込みプロファイルは7つです(16.3.6 のドキュメントより)。
| プロファイル | 用途の目安 | stale |
revalidate |
expire |
|---|---|---|---|---|
default(指定なし) |
標準 | 5分 | 15分 | なし |
seconds |
ほぼリアルタイム | 30秒 | 1秒 | 1分 |
minutes |
頻繁に変わる | 5分 | 1分 | 1時間 |
hours |
1日に何度か変わる | 5分 | 1時間 | 1日 |
days |
1日1回程度 | 5分 | 1日 | 1週間 |
weeks |
週1回程度 | 5分 | 1週間 | 30日 |
max |
ほとんど変わらない | 5分 | 30日 | 1年 |
// その場だけの指定(省略した値は default から引き継ぐ)
cacheLife({ stale: 60, revalidate: 300, expire: 3600 });
短すぎるキャッシュは静的シェルに入らず、リクエスト時に解決される「動的な穴」になります。
| 条件 | 扱い |
|---|---|
revalidate が 0、または expire が5分未満 |
プリレンダーから除外(動的な穴)→ <Suspense> が必要 |
stale が30秒未満 |
プリレンダーから除外 |
stale が30秒以上5分未満 |
プリレンダーには入るが、App Shell からは除外 |
組み込みでは seconds だけがこれに当たります(expire 1分)。また、短いキャッシュを、cacheLife を書いていない外側の "use cache" の中で使うとビルドエラーになります。 外側にも cacheLife を明示して直します。公式ドキュメントも「すべての "use cache" に cacheLife を書く」ことをすすめています。
cacheTag と無効化の4関数の使い分け
データを変えたら、キャッシュを捨てる関数を呼びます。 4つの違いを、商品一覧で実際に比べました。
// app/products/actions.ts
"use server";
import { refresh, revalidatePath, revalidateTag, updateTag } from "next/cache";
import { addProduct } from "@/lib/db";
function nameOf(formData: FormData) {
return String(formData.get("name") ?? "").trim() || "無名";
}
export async function addNoInvalidate(formData: FormData) {
await addProduct(nameOf(formData)); // キャッシュには何もしない
}
export async function addWithUpdateTag(formData: FormData) {
await addProduct(nameOf(formData));
updateTag("products"); // すぐ期限切れ → この応答で最新が見える
}
export async function addWithRevalidateTag(formData: FormData) {
await addProduct(nameOf(formData));
revalidateTag("products", "max"); // 古い内容を返しつつ裏で作り直す
}
export async function addWithRevalidatePath(formData: FormData) {
await addProduct(nameOf(formData));
revalidatePath("/products"); // パス単位で捨てる
}
export async function refreshOnly() {
refresh(); // キャッシュは触らず、キャッシュしていない部分だけ取り直す
}
ページには、キャッシュした一覧(getProducts())と、比較用にキャッシュしない件数(await connection() のあとで直接読む)を並べました。
// app/products/page.tsx(抜粋)
import { Suspense } from "react";
import { connection } from "next/server";
import { getProducts } from "@/lib/products";
import { readProducts } from "@/lib/db";
import * as a from "./actions";
async function CachedList() {
const { items, generatedAt } = await getProducts();
return <p id="cached-meta">cached: {items.length}件 / generatedAt {generatedAt}</p>;
}
async function LiveCount() {
await connection(); // ここから先はリクエスト時に実行
const items = await readProducts(); // キャッシュしない
return <p id="live-count">live: {items.length}件</p>;
}
export default function ProductsPage() {
return (
<main>
<CachedList />
<Suspense fallback={<p>counting…</p>}>
<LiveCount />
</Suspense>
<form>
<input name="name" defaultValue="付箋" />
<button formAction={a.addNoInvalidate}>追加(無効化なし)</button>
<button formAction={a.addWithUpdateTag}>追加+updateTag</button>
<button formAction={a.addWithRevalidateTag}>追加+revalidateTag</button>
<button formAction={a.addWithRevalidatePath}>追加+revalidatePath</button>
<button formAction={a.refreshOnly}>refresh</button>
</form>
</main>
);
}
Playwright でボタンを順に押し、応答後の表示を読み取った結果です(ビルド直後、データ2件から開始)。
| 操作 | キャッシュした一覧 | キャッシュしない件数 |
|---|---|---|
| 初回表示 | 2件 / 06:08:08 | 2件 |
| 無効化なしで追加 → 直後 | 2件 / 06:08:08 | 2件 |
refresh() → 直後 |
2件 / 06:08:08(そのまま) | 3件(取り直された) |
updateTag で追加 → 直後 |
4件 / 06:08:18 | 4件 |
revalidateTag(…, "max") で追加 → 直後 |
4件 / 06:08:18(そのまま) | 4件(再描画なし) |
| → 再読み込み1回目 | 4件 / 06:08:18(古い内容) | 5件 |
| → 再読み込み2回目 | 5件 / 06:08:20 | 5件 |
revalidatePath で追加 → 直後 |
6件 / 06:08:22 | 6件 |
ここから読み取れる使い分けは次のとおりです。
| 関数 | 呼べる場所 | 応答で画面が再描画されるか | 使いどころ |
|---|---|---|---|
updateTag(tag) |
Server Action のみ | される(最新を待って描画) | フォーム送信後に自分の変更をすぐ見せる |
revalidateTag(tag, "max") |
Server Action・Route Handler | されない。次の読み取りは古い内容、その次から新しい内容 | CMS の Webhook、少し遅れてよい一覧 |
revalidatePath(path) |
Server Action・Route Handler | される | タグを付けていない、1ページだけ更新 |
refresh() |
Server Action のみ | される(キャッシュは使ったまま) | キャッシュしていない数値だけ更新 |
「無効化なし」は、DB は増えていても画面は変わりません。 キャッシュの外側(LiveCount)も、応答に再描画が含まれないため 2件のままでした。
updateTag を Route Handler で呼ぶと
外部サービスからの Webhook などは Route Handler で受けます。そこで updateTag を呼ぶと、実行時に次のエラーになりました。
⨯ Error: updateTag can only be called from within a Server Action. To invalidate cache tags in Route Handlers or other contexts, use revalidateTag instead. See more info here: https://nextjs.org/docs/app/api-reference/functions/updateTag
Route Handler では revalidateTag(tag, "max") を使います。 こちらは {"ok":true,"mode":"tag"} が返り、正常に動きました。
Suspense なしで未キャッシュデータを読んだときのエラーと直し方3パターン
Cache Components では、キャッシュしていないデータを <Suspense> の外で待つとビルドが止まります。 次のページでビルドしました(readProducts() はファイルを読む非同期関数)。
// app/broken/page.tsx
import { readProducts } from "@/lib/db";
// ❌ 'use cache' も <Suspense> もなしで、遅いデータを待っている
export default async function Broken() {
const products = await readProducts();
return <ul>{products.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}
Error: Route "/broken": Next.js encountered uncached or runtime data during prerendering.
`fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`, or `connection()` accessed outside of `<Suspense>` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.
Ways to fix this:
- [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access
- [cache] For uncached data (`fetch`, database calls): cache the access with `"use cache"` (does not apply to `connection()`)
- [block] Set `export const instant = false` to allow a blocking route
Learn more: https://nextjs.org/docs/messages/blocking-prerender-dynamic
エラー文そのものが3つの直し方を示しています。 それぞれ実際に直してビルドしました。
パターン1 [stream]:Suspense で囲む
import { Suspense } from "react";
import { readProducts } from "@/lib/db";
async function List() {
const products = await readProducts();
return <ul>{products.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}
// [stream] データを待つ部分だけ <Suspense> で囲む
export default function Page() {
return (
<main>
<h1>在庫一覧</h1>
<Suspense fallback={<p>読み込み中…</p>}>
<List />
</Suspense>
</main>
);
}
パターン2 [cache]:use cache でキャッシュする
import { cacheLife } from "next/cache";
import { readProducts } from "@/lib/db";
// [cache] データ取得を "use cache" でキャッシュする
async function getList() {
"use cache";
cacheLife("hours");
return readProducts();
}
export default async function Page() {
const products = await getList();
return <ul>{products.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}
パターン3 [block]:待つページだと明示する
import { readProducts } from "@/lib/db";
// [block] このページはデータがそろうまで待つ(読み込み表示なし)と明示する
export const instant = false;
export default async function Page() {
const products = await readProducts();
return <ul>{products.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}
3つとも同時にビルドが通り、ルート表では次のように表示されました。
Route (app) Revalidate Expire
├ ƒ /fix-block
├ ○ /fix-cache 1h 1d
├ ◐ /fix-stream
| パターン | 表示 | 最初の表示 | 選ぶ基準 |
|---|---|---|---|
| [stream] Suspense | ◐ 部分プリレンダー |
骨組みが即表示、一覧はあとから | 最新のデータを見せたい |
| [cache] use cache | ○ 静的(1h / 1d) |
一覧まで即表示 | 1時間程度の遅れを許せる |
| [block] instant = false | ƒ 動的 |
そろうまで何も出ない | 移行途中で一時的に許可したい |
迷ったら [stream] か [cache] を選びます。 [block] は、既存アプリを段階的に移行するときの一時的な逃げ道として用意されています(アップグレード用 codemod もこの行を追加します)。
ほかに実際に出たエラー
| 書いたコード | エラー(実文の冒頭) | 直し方 |
|---|---|---|
Server Component で new Date() をそのまま表示 |
Error: Route "/broken2": Next.js encountered the unstable value `new Date()` while prerendering. |
先に await connection()、または "use cache"、または Client Component で表示 |
"use cache" の中で cookies() |
Error: Route /broken3 used `cookies()` inside "use cache". Accessing Dynamic data sources inside a cache scope is not supported. |
キャッシュの外で読み、値を引数で渡す |
Route Handler で updateTag() |
Error: updateTag can only be called from within a Server Action. |
revalidateTag(tag, "max") |
new Date() のエラーには [dynamic] [cache] [client] [measure](計測目的なら performance.now())の4つの直し方が表示されました。cookie を使う場合の正しい形は次のとおりです。
import { Suspense } from "react";
import { cookies } from "next/headers";
import { cacheLife } from "next/cache";
async function Greeting({ name }: { name: string }) {
"use cache";
cacheLife("hours");
return <p>Hello {name}</p>; // name が引数なので、名前ごとに別のキャッシュになる
}
async function GreetingWithCookie() {
const name = (await cookies()).get("name")?.value ?? "guest"; // キャッシュの外で読む
return <Greeting name={name} />;
}
export default function Page() {
return (
<Suspense fallback={<p>loading</p>}>
<GreetingWithCookie />
</Suspense>
);
}
実際に next start で確かめると、cookie name=tanaka 付きのリクエストでは Hello tanaka、cookie なしでは Hello guest と表示され、ビルド表示は ◐ /greet でした。
ビルド出力のルート表の読み方
商品一覧・時刻ページなどを入れたアプリの next build の出力です。
▲ Next.js 16.3.6 (Turbopack)
- Cache Components enabled
Route (app) Revalidate Expire
┌ ○ /
├ ○ /_not-found
├ ƒ /api/revalidate
├ ◐ /products 5m 1d
└ ◐ /time 30d 1y
○ (Static) prerendered as static content
◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content
ƒ (Dynamic) server-rendered on demand
| 記号・列 | 意味 | この例では |
|---|---|---|
○ Static |
すべてビルド時に作った | /、/fix-cache |
◐ Partial Prerender |
静的シェル+あとから流す部分 | /products(件数を Suspense で流す) |
ƒ Dynamic |
リクエストのたびに作る | Route Handler、instant = false のページ |
Revalidate / Expire |
そのページで最も短い cacheLife |
/products は独自の catalog(300秒・1日) |
/time が 30d / 1y(max の値)なのは、10秒のキャッシュが「動的な穴」としてシェルから外れ、シェルに残ったのが max の部分だけだからです。
Playwright で時刻を見て、キャッシュの効き方を確かめる
3種類の時刻を並べたページを作り、時間をおいて開き直しました。
// app/time/page.tsx
import { Suspense } from "react";
import { connection } from "next/server";
import { cacheLife } from "next/cache";
const fmt = (d: Date) => d.toISOString().slice(11, 19); // HH:MM:SS(UTC)
// ① ビルド時に1回だけ作られ、静的シェルに入る
async function BuildTime() {
"use cache";
cacheLife("max");
return <p id="build">build: {fmt(new Date())}</p>;
}
// ② 10秒ごとに作り直す短いキャッシュ(expire が5分未満なので「動的な穴」になる)
async function ShortCache() {
"use cache";
cacheLife({ stale: 30, revalidate: 10, expire: 60 });
return <p id="short">short: {fmt(new Date())}</p>;
}
// ③ キャッシュしない:リクエストのたびに作る
async function Live() {
await connection();
return <p id="live">live: {fmt(new Date())}</p>;
}
export default function TimePage() {
return (
<main>
<h1>Time</h1>
<BuildTime />
<Suspense fallback={<p>loading short…</p>}>
<ShortCache />
</Suspense>
<Suspense fallback={<p>loading live…</p>}>
<Live />
</Suspense>
</main>
);
}
Playwright(playwright-core + Chromium)で、0秒・3秒後・さらに11秒後・1秒後・1秒後に開き、3つの時刻を読み取りました。
t=0.6s | build: 06:06:35 | short: 06:06:58 | live: 06:06:58
t=4.0s | build: 06:06:35 | short: 06:06:58 | live: 06:07:02
t=15.5s | build: 06:06:35 | short: 06:07:13 | live: 06:07:13
t=16.9s | build: 06:06:35 | short: 06:07:13 | live: 06:07:15
t=18.3s | build: 06:06:35 | short: 06:07:13 | live: 06:07:16
- build:ずっとビルド時刻のまま。静的シェルに焼き込まれています。
- short:10秒以内は同じ値、10秒を過ぎたあとの読み込みで新しい時刻になり、また同じ値が続きます。
- live:開くたびに変わります。
つまずきやすいポイント
| つまずき | 起きること | こうするとよい |
|---|---|---|
cacheLife を書かない |
default(15分)になり、入れ子では内側の短い期間に引っ張られる |
すべての "use cache" に cacheLife を書く |
React.cache で値を渡す |
"use cache" の中からは見えない(別スコープ) |
引数で渡す |
| サーバーレスで実行時キャッシュ | 既定のメモリキャッシュはインスタンス間で共有されない | 必要なら "use cache: remote" やキャッシュハンドラー |
| デプロイし直す | キーにビルドIDが入るため、キャッシュは引き継がれない | デプロイをまたぐなら fetch キャッシュ等を検討 |
next start を再起動しただけ |
作り直した結果が .next に残り、前の内容のまま見える(実測) |
検証をやり直すときは next build から |
| 挙動を詳しく見たい | — | NEXT_PRIVATE_DEBUG_CACHE=1 npm run start で詳細ログ |
よくある質問
Q. "use cache" と unstable_cache はどちらを使えばよいですか?
A. Cache Components を使うなら "use cache" です。コンパイラーがキーを自動で作り、コンポーネントにも使えます。デプロイをまたいで残したいデータには、unstable_cache や fetch のキャッシュが案内されています。
Q. cacheComponents: true にしないと "use cache" は使えませんか?
A. はい。16 では "use cache"・cacheLife・cacheTag は Cache Components の機能として提供されています。
Q. フォーム送信後、updateTag と revalidatePath のどちらがよいですか?
A. 読み取り側に cacheTag を付けているなら updateTag が狙いを絞れて明確です。タグを付けていない既存ページなら revalidatePath が手軽です。どちらも応答の中で画面が更新されることを実測で確認しました。
Q. revalidateTag の第2引数は何を渡せばよいですか?
A. 公式は多くの場合 'max' をすすめています。'hours' などのプロファイル名や { expire: 3600 } も渡せます。1引数の形は 16 で非推奨です。
まとめ
cacheComponents: trueにすると既定はリクエストごとの実行になり、"use cache"を書いた所だけがキャッシュされる。"use cache"は関数・コンポーネント・ファイルの3か所に置け、キーは引数から自動で作られる。cacheLifeは7つの組み込みプロファイル+独自プロファイル。すべてのキャッシュに明示するのが公式のおすすめ。updateTagは即時反映、revalidateTag(tag, "max")は次の次から反映、refreshは未キャッシュ部分だけと、実測で違いがはっきり出た。- Suspense なしで未キャッシュデータを待つとビルドエラー。直し方は [stream]・[cache]・[block] の3つで、エラー文にも表示される。
- ルート表の
○◐ƒと Revalidate / Expire 列で、各ページの描画方法とキャッシュ期間が確認できる。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Next.js use cacheの使い方|Cache Components実測ガイド」
株式会社RJCでは、新しいキャッシュの仕組みも小さなアプリで実際に動かし、結果を見ながら仲間と理解を深めています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。