# Vercel キャッシュ戦略ガイド：ISR・CDN Cache・Runtime Cache・Cache Components(PPR) の4層を使い分ける

> Vercel公式に忠実なキャッシュ実装ガイド。静的キャッシュ・ISR（stale-while-revalidate/31日永続/300msグローバルpurge/リクエスト併合）・CDN Cache（Cache-Control/CDN-Cache-Control/Vercel-CDN-Cache-Controlの3ヘッダ）・Runtime Cache・Cache Components(PPR)の4層を、on-demand revalidation（revalidatePath/revalidateTag/updateTag）とx-vercel-cacheの読み方まで実コードで解説します。

- 公開日: 2026-06-28
- 著者: 友田 陽大
- タグ: Vercel, Next.js, パフォーマンス, ISR, キャッシュ, コスト最適化, フロントエンド
- URL: https://tomodahinata.com/blog/vercel-caching-isr-cache-components-ppr-guide
- カテゴリ: Vercel 本番運用
- 総合ガイド: https://tomodahinata.com/blog/vercel-production-platform-guide

## 要点

- Vercelのキャッシュは4層——『静的（自動）／ISR（フレームワーク連携）／CDN Cache（Cache-Controlヘッダ）／Runtime Cache（関数内データ）』。レスポンス全体か個別データ片か、フレームワーク連携か手動かで選ぶ
- ISRはstale-while-revalidateで、31日間の永続キャッシュ・300msのグローバルpurge・リクエスト併合（同一未キャッシュパスへの同時アクセスをリージョンごと1回の関数呼び出しに併合）・即時ロールバック耐性を自動で得る。Next.js/SvelteKit/Nuxt/Astroで動く
- オンデマンド再検証はrevalidatePath/revalidateTagで即時反映。Cache Components(PPR)時代はcacheTagとupdateTagで、静的シェルと動的穴を両立しつつタグ単位で無効化する
- 関数レスポンスのCDNキャッシュはCache-Control（ブラウザ）・CDN-Cache-Control（下流CDN）・Vercel-CDN-Cache-Control（Vercelのみ）の3ヘッダで階層制御。s-maxageとstale-while-revalidateが要。Vercel-CDN-Cache-Controlはブラウザに返らない
- 結果はx-vercel-cacheヘッダ（HIT/MISS/STALE/PRERENDER）で必ず検証する。Authorization/Set-Cookie付き・10MB超・privateはキャッシュされない

---

「ページが遅い」の解決策は1つではありません。Vercel は**4つのキャッシュ層**を提供しており、「レスポンス全体か／個別データ片か」「フレームワーク連携か／手動制御か」で使い分けます。層を取り違えると、**キャッシュされていないのにされていると思い込む**（そして Active CPU 課金とレイテンシで損をする）か、逆に**古いデータを出し続ける**事故になります。

この記事は [ISR](https://vercel.com/docs/incremental-static-regeneration) と [CDN Cache](https://vercel.com/docs/edge-network/caching)、[Runtime Cache](https://vercel.com/docs/runtime-cache) の公式仕様に忠実に、4層を実コードで整理します。Next.js の Cache Components / PPR そのものの深掘りは [Next.js 16 App Router の Cache Components 記事](/blog/nextjs-16-app-router-cache-components-data-fetching) に譲り、本稿は **「Vercel プラットフォームとしてどうキャッシュが効くか」** に集中します。全体像は [Vercel 本番運用ガイド](/blog/vercel-production-platform-guide) を参照してください。

---

## 4層の地図：まずどれを使うか

| 層 | 対象 | 設定 | 永続性 | 代表的な用途 |
|---|---|---|---|---|
| **静的ファイル** | ビルド出力（JS/CSS/画像/フォント） | 自動 | デプロイ寿命（ハッシュ名で跨ぎ持続） | 全デプロイ。考えなくてよい |
| **ISR** | ページ（HTML＋データ） | フレームワークAPI（`revalidate`等） | **31日**・再検証まで | スケジュール更新のページ（EC・CMS・生成系） |
| **CDN Cache** | 関数のHTTPレスポンス | `Cache-Control` 系ヘッダ | 最大1年（ベストエフォート） | ISR非対応・APIレスポンスを地域別にキャッシュ |
| **Runtime Cache** | 関数**内**のデータ片 | Runtime Cache API | タグ無効化まで | fetch結果・DBクエリ・計算値の再利用 |

判断のショートカット：

- **ページ全体**を出すフレームワーク（Next.js等）→ **ISR**（最強の機能群が自動で付く）
- **API のレスポンス**を地域キャッシュ → **CDN Cache（Cache-Control）**
- 関数の中で**同じデータを何度も取る** → **Runtime Cache**
- 静的アセット → 何もしない（**自動**）

---

## ISR：最も多機能なキャッシュ

### stale-while-revalidate の挙動

ISR は「キャッシュ済みを即返し、裏で再生成」。Vercel の CDN に乗ることで、フレームワークの ISR は次を**自動で**得ます（[ISR docs](https://vercel.com/docs/incremental-static-regeneration)）。

- **31日間の永続ストレージ**：Function リージョンに永続。デプロイごとに独立したキャッシュ。
- **300ms のグローバル purge**：再検証時、全リージョンが 300ms 以内に更新。HTML とデータを**アトミックに**差し替える（全ページ遷移とクライアント遷移で内容が食い違わない）。
- **リクエスト併合**：未キャッシュの同一パスに同時アクセスが殺到しても、リージョンごとに**関数を1回だけ**呼ぶ。スパイク時のオリジン保護。
- **即時ロールバック耐性**：過去デプロイのキャッシュは消えないので、ロールバックしても生成済みコンテンツを失わない。
- **キャッシュシールド**：CDN ミス時、関数を呼ぶ前に永続 ISR キャッシュを読む。

```ts
// app/blog/[slug]/page.tsx
// 時間ベース再検証：3600秒ごとに裏で再生成
export const revalidate = 3600;

// 人気ページはビルド時に事前生成、それ以外はオンデマンド生成
export async function generateStaticParams() {
  const popular = await getPopularSlugs();
  return popular.map((slug) => ({ slug }));
}

export default async function Post({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  return <Article post={await getPost(slug)} />;
}
```

### オンデマンド再検証：イベントで即時反映

「3600秒待たず、CMS で公開した瞬間に反映したい」——これがオンデマンド再検証です。`revalidatePath`／`revalidateTag` を使います。

```ts
// app/api/revalidate/route.ts — CMS の Webhook から叩く
import { revalidatePath, revalidateTag } from "next/cache";

export async function POST(request: Request) {
  // ① 認可（無防備な再検証エンドポイントは攻撃面になる）
  if (request.headers.get("authorization") !== `Bearer ${process.env.REVALIDATE_SECRET}`) {
    return new Response("Unauthorized", { status: 401 });
  }

  const { slug, tag } = await request.json();

  if (slug) revalidatePath(`/blog/${slug}`); // パス単位
  if (tag) revalidateTag(tag);               // タグ単位（複数ページ横断）

  return Response.json({ revalidated: true });
}
```

データ取得側でタグを付けておくと、`revalidateTag("posts")` のように**横断的に無効化**できます。

```ts
// fetch にタグを付ける（Next.js）
const posts = await fetch("https://cms.example.com/posts", {
  next: { tags: ["posts"], revalidate: 3600 },
}).then((r) => r.json());
```

> **再検証が失敗したら？** Vercel はステイルを返し続け、**30秒の TTL** で再試行します。再検証成功とみなすステータスは `200/301/302/307/308/404/410` のみ。これ以外・ネットワークエラー・関数エラーは失敗扱いです。だから「再検証先が一時的に落ちても、ユーザーには古いページが出続ける」——可用性に効く設計です。

### 対応フレームワーク

ISR は Next.js 専用ではありません。

| フレームワーク | 有効化 |
|---|---|
| Next.js（App Router） | ルートセグメントから `revalidate` を export |
| Next.js（Pages Router） | `getStaticProps` が `revalidate` を返す |
| SvelteKit | `config` に `isr` プロパティ |
| Nuxt | `routeRules` に `isr` |
| Astro | server 出力で ISR 設定 |
| Gatsby | DSG（Deferred Static Generation） |

---

## Cache Components（PPR）：静的シェルと動的穴の両立

Next.js 16 の **Cache Components（Partial Prerendering, PPR）** は、「1ページの中で、静的な殻（シェル）を即配信しつつ、動的な部分だけ穴を空けてストリームする」モデルです。`use cache` ディレクティブで境界を宣言し、`cacheLife`／`cacheTag` で寿命とタグを制御します。

```tsx
// app/dashboard/page.tsx
import { Suspense } from "react";

export default function Dashboard() {
  return (
    <>
      <StaticHeader />               {/* 静的シェル：即配信 */}
      <Suspense fallback={<Skel />}>
        <UserStats />               {/* 動的な穴：ストリーム */}
      </Suspense>
    </>
  );
}
```

```tsx
// キャッシュ境界を宣言（コンポーネント/関数単位）
async function getCatalog() {
  "use cache";
  cacheLife("hours");     // 寿命プロファイル
  cacheTag("catalog");    // タグ付け → updateTag/revalidateTag で無効化
  return db.products.findMany();
}
```

Vercel 上では、この静的シェルが ISR/CDN に乗り、動的穴は Fluid Compute の関数がストリーミングで埋めます。**タグ単位の無効化**（`updateTag`/`revalidateTag`）で、書き込み後に該当タグだけ即時更新できます。

> Cache Components の API（`use cache`・`cacheLife`・`cacheTag`・`updateTag`・`unstable_cache` からの移行）の詳細は、フロントエンドクラスタの [Next.js 16 Cache Components 記事](/blog/nextjs-16-app-router-cache-components-data-fetching) に集約しています。本稿では「Vercel 上でどう配信されるか」だけ押さえれば十分です。

---

## CDN Cache：関数レスポンスを3ヘッダで制御

ISR を使わない API・SSR でも、`Cache-Control` を返せば**そのリージョンの CDN にキャッシュ**されます。Vercel は**ブラウザ／下流CDN／Vercel CDN** を別々に制御できる3ヘッダを提供します（[CDN Cache docs](https://vercel.com/docs/edge-network/caching)）。

```ts
// app/api/catalog/route.ts
export async function GET() {
  return Response.json(await getCatalog(), {
    headers: {
      "Cache-Control": "public, max-age=10",        // ブラウザ：10秒
      "CDN-Cache-Control": "public, s-maxage=60",   // 下流CDN：60秒
      "Vercel-CDN-Cache-Control": "public, s-maxage=3600", // Vercel：3600秒
    },
  });
}
```

| ヘッダ | 制御先 | ブラウザに返るか |
|---|---|---|
| `Cache-Control` | ブラウザ＋（CDN指定がなければ）CDN | ✅ |
| `CDN-Cache-Control` | 下流CDN・Vercel CDN（ブラウザは無視） | ✅ |
| `Vercel-CDN-Cache-Control` | **Vercel CDN のみ** | ❌（返らない・転送されない） |

関数レスポンスを CDN にキャッシュするには `s-maxage=N`（任意で `, stale-while-revalidate=Z`）が必要です。`CDN-Cache-Control` を指定せず `Cache-Control` だけ書くと、Vercel はブラウザ送信前に `s-maxage`/`stale-while-revalidate` を**剥がします**（ブラウザに内部キャッシュ方針を漏らさないため）。

### キャッシュ可能条件（厳格）

レスポンスが CDN にキャッシュされるには、**すべて**を満たす必要があります。

- リクエストが `GET` または `HEAD`、`Range`/`Authorization` ヘッダを含まない
- レスポンスが `200/404/410/301/302/307/308`
- コンテンツ長 10MB 以下（**ストリーミングは 20MB**）
- `Set-Cookie` を含まない
- `Cache-Control` に `private`/`no-cache`/`no-store` を含まない
- `Vary: *` を含まない

> **`Authorization` と `Set-Cookie` が鬼門**：認証付き API がキャッシュされない最頻原因です。ログイン必須ページは基本キャッシュしない（または `Vary` とユーザー非依存部分の分離で設計）。

### Vary でユーザー属性ごとに分ける

国・言語・デバイスで内容を変えつつキャッシュしたいときは `Vary`。Vercel は `X-Vercel-IP-Country` 等のリクエストヘッダをキャッシュキーに加えられます。

```ts
export async function GET(request: Request) {
  const country = request.headers.get("x-vercel-ip-country") ?? "unknown";
  return Response.json({ greeting: `Hello from ${country}` }, {
    headers: { "Cache-Control": "s-maxage=3600", Vary: "X-Vercel-IP-Country" },
  });
}
```

`Vary` は1つ増えるごとにキャッシュエントリが指数的に増える（＝ヒット率が下がる）ので、**本当に内容を変えるヘッダだけ**に絞ります。

---

## Runtime Cache：関数内のデータ片をキャッシュ

レスポンス全体ではなく、関数の**中**で「同じ fetch・同じ DB クエリ・同じ計算結果」を再利用したいとき。Runtime Cache はリージョンごとのエフェメラルな KV で、**タグで無効化**でき、Functions・Routing Middleware・Builds 間で共有されます。

```ts
// 高コストな計算を Runtime Cache に載せる（疑似コード）
import { getCache } from "@vercel/functions";

export async function GET() {
  const cache = getCache();
  const key = "exchange-rates";

  const cached = await cache.get(key);
  if (cached) return Response.json(cached); // データ片の再利用

  const rates = await fetchExchangeRatesFromUpstream(); // 高コスト
  await cache.set(key, rates, { tags: ["rates"], ttl: 300 });
  return Response.json(rates);
}
```

ISR や `Cache-Control` が「レスポンス全体」を扱うのに対し、Runtime Cache は「**個別のデータ片**」を扱うのが本質的な違いです。両者は**併用**できます（ページは ISR、ページ内の一部 fetch は Runtime Cache）。

---

## デバッグ：x-vercel-cache を読む

「キャッシュされているつもりで、実は毎回関数が走っている」を防ぐ唯一の方法は、**`x-vercel-cache` ヘッダを実際に見る**ことです。

```bash
curl -sI https://your-app.vercel.app/api/catalog | grep -i x-vercel-cache
# x-vercel-cache: HIT
```

| 値 | 意味 |
|---|---|
| `HIT` | CDN キャッシュから配信（関数は走っていない） |
| `MISS` | キャッシュになく、オリジン（関数）で生成 |
| `STALE` | 期限切れだがステイルを配信、裏で再検証中 |
| `PRERENDER` | ビルド時に事前生成された静的コンテンツ |

本番リリース前に、**キャッシュさせたいパスが `HIT`/`PRERENDER` になっているか**を必ず確認してください。これが Active CPU 課金とレイテンシを直接左右します（[コスト最適化ガイド](/blog/vercel-cost-active-cpu-pricing-optimization-guide)）。

---

## 本番チェックリスト（キャッシュ）

- [ ] キャッシュ層を**4つから意図的に選択**している（なんとなく SSR ではない）
- [ ] ISR の `revalidate` を要件に合わせて設定、人気ページは `generateStaticParams`
- [ ] オンデマンド再検証エンドポイントを**認可**している
- [ ] 関数キャッシュは `s-maxage` を付け、3ヘッダの役割を理解
- [ ] 認証付きレスポンスを**誤ってキャッシュしていない**（Authorization/Set-Cookie）
- [ ] `Vary` は本当に内容を変えるヘッダだけ
- [ ] `x-vercel-cache` で `HIT`/`PRERENDER` を**実測確認**
- [ ] CWV（LCP/INP/CLS）を Speed Insights で監視（[CWV 最適化](/blog/core-web-vitals-nextjs-inp-lcp-cls-optimization-guide)）

---

## まとめ

キャッシュは「速さ」だけでなく、**コスト（Active CPU を呼ばない）と可用性（再検証失敗時もステイル配信）** を同時に作ります。

1. **層を選ぶ**：ページは ISR、API は CDN Cache、データ片は Runtime Cache、静的は自動
2. **ISR の自動最適化**（31日永続・300ms purge・リクエスト併合・ロールバック耐性）を活かす
3. **オンデマンド再検証**で即時反映、Cache Components でシェルと穴を両立
4. **3ヘッダ**で階層制御し、キャッシュ可能条件を満たす
5. **`x-vercel-cache` で必ず実測**する

次は、作ったものを安全に出す [デプロイ・CI/CD・ロールバック ガイド](/blog/vercel-deployments-cicd-rollback-rolling-releases-guide) へ。

> 本記事は [ISR](https://vercel.com/docs/incremental-static-regeneration) / [CDN Cache](https://vercel.com/docs/edge-network/caching) / [Runtime Cache](https://vercel.com/docs/runtime-cache) 公式ドキュメント（2026年6月時点）に基づきます。仕様は更新されるため、本番採用時は公式で最新値を確認してください。
