デプロイは午前2時14分に完了します。91,000個の商品ページ(カテゴリツリー、14市場のローカライズ版、編集SEOコンテンツ)がNext.js 14のApp Routerからv16のcacheComponents APIに切り替わります。Vercelのログで最初のリクエストウォーターフォールを見ます。TTFBが1.8秒に急上昇します。Slackが鳴ります。18ヶ月間、古いデフォルトキャッシュモデルと戦ってきました:古い価格設定、遅すぎるrevalidateTag呼び出し、「ウェブサイトのせい」と言う顧客サポートチケット。Next.js 15はキャッシングをデフォルトでオフに切り替えました。v16はあなたに外科的にオプトバックインするためのcacheComponentsを与えました。あなたは遅い死よりも移行を選びました。今、あなたは実際のトラフィック、実際のエラー、そして日の出の2時間前に本番インシデントを凝視しています。ここで何が壊れたのか、何が保持されたのか、そしてベンチマークスイートが予測しなかった性能差分があります。

目次

Next.js 16 cacheComponents: App Routerキャッシングから91,000ページを移行

実際にあったキャッシング問題

状況を説明しましょう。Next.js 14のApp Routerでは、Server Componentsのfetchリクエストはデフォルトでキャッシュされていました。Data Cacheはデプロイ全体で永続化されていました。Full Route Cacheはレンダリング済みHTMLとRSCペイロードをビルド時に保存していました。クライアント側のRouter Cacheは、予想以上に長くプリフェッチされたセグメントを保持していました。

91,000ページのサイトでは、このデフォルトキャッシュ方式は2つのカテゴリの問題を作成していました:

至る所で古いデータ。 商品価格はヘッドレスCMS(私たちの場合はSanity)で更新されていましたが、キャッシュされたフェッチ結果は残っていました。47個の異なるサーバーアクション全体にrevalidateTag呼び出しが散乱していました。1つのタグを見逃したら?顧客は昨日の価格を見ます。私たちは実際に#cache-crimesと呼ばれるSlackチャネルを持っていて、コンテンツチームが古いページを報告していました。

地獄のようなビルド時間。 91,000ページの完全な静的生成には3時間以上かかりました。ほとんどのページでrevalidate: 3600を使用するISRに移行していましたが、ISR、Data Cache、オンデマンド再検証間の相互作用は本当に理解するのが難しかったです。チームの新しい開発者は最初の2週間をキャッシング層を理解するだけに費やすことになるでしょう。

メンタルモデルのコスト

人々が過小評価していると思うことはここにあります:暗黙的なキャッシングの認知的コスト。キャッシングがデフォルトで、あなたがオプトアウトしたら、すべての新しいコンポーネントは「これはキャッシュされるべきか?」と尋ねる必要があります。そして答えがいいえなら正しいディレクティブを追加することを忘れない。キャッシングがデフォルトでなく、あなたがオプトインしたら、あなたはアクティブにそれを望むときだけキャッシングについて考えます。それは本質的に異なった、そしてより良い、メンタルモデルです。

Next.js 15と16の変更点

Next.js 15は大きな哲学的転換でした。チームはデフォルトを反転させました:

動作 Next.js 14 Next.js 15 Next.js 16
Server Componentsのfetch() デフォルトでキャッシュ デフォルトでキャッシュなし デフォルトでキャッシュなし
Route Handlers (GET) デフォルトでキャッシュ デフォルトでキャッシュなし デフォルトでキャッシュなし
Client Router Cache 30秒(動的)/ 5分(静的) ページセグメント0秒 デフォルト0秒、設定可能
Full Route Cache 静的ルートで有効 同じ 同じ、cacheLifeの改善付き
コンポーネントレベルキャッシング unstable_cache use cacheディレクティブ(実験的) cacheComponents API(安定)

Next.js 15はuse cacheディレクティブをフラグの背後にある実験的機能として導入しました。Next.js 16(2025年初めリリース)は、これをcacheComponents設定オプションと関連する"use cache"ディレクティブ、カスタムキャッシュプロフィールを定義するためのcacheLife、ターゲット化された無効化用のcacheTagとして安定化しました。

重要な洞察:**キャッシングは暗黙的なフレームワーク動作からコンポーネントレベルでの明示的な開発者選択に移った。**これは大規模なサイトにとって大きな取引です。

cacheComponentsを理解する

Next.js 16のcacheComponents機能は"use cache"ディレクティブを通じてコンポーネントレベルのキャッシングを有効にします。基本的なセットアップはここにあります:

// next.config.js (Next.js 16)
const nextConfig = {
  experimental: {
    cacheComponents: true,
  },
};

module.exports = nextConfig;

有効にすると、任意の非同期Server Component、サーバーアクション、またはレイアウトファイルの上部に"use cache"を追加できます:

// app/products/[slug]/page.tsx
"use cache";

import { cacheLife, cacheTag } from 'next/cache';

export default async function ProductPage({ params }: { params: { slug: string } }) {
  cacheLife('products'); // カスタムキャッシュプロフィール
  cacheTag(`product-${params.slug}`);

  const product = await fetchProduct(params.slug);
  
  return (
    <div>
      <h1>{product.name}</h1>
      <ProductDetails product={product} />
      <DynamicPricing productId={product.id} /> {/* このコンポーネントはキャッシュされません */}
    </div>
  );
}

cacheLifeプロフィール

ここが大規模サイトにとって興味深くなるところです。next.config.jsに名前付きキャッシュプロフィールを定義します:

const nextConfig = {
  experimental: {
    cacheComponents: true,
    cacheLife: {
      products: {
        stale: 300,      // 5分間古いものを提供
        revalidate: 3600, // 1時間後に再検証
        expire: 86400,    // 24時間後にハード削除
      },
      editorial: {
        stale: 3600,
        revalidate: 86400,
        expire: 604800,   // 7日
      },
      navigation: {
        stale: 86400,
        revalidate: 604800,
        expire: 2592000,  // 30日
      },
    },
  },
};

3層モデル(stalerevalidateexpire)はstale-while-revalidateセマンティクスにうまくマップされています。staleウィンドウの間、キャッシュされたコンテンツは即座に提供されます。staleの後ですがexpireの前に、バックグラウンド再検証が開始されます。expireの後、キャッシュエントリは消えます。

無効化のためのcacheTag

cacheTag関数は古いrevalidateTagパターンを、より構成可能なものに置き換えます:

import { revalidateTag } from 'next/cache';

// ウェブフックハンドラーまたはサーバーアクションの中で:
export async function handleProductUpdate(productSlug: string) {
  revalidateTag(`product-${productSlug}`);
  revalidateTag('product-listing'); // リストページも無効化
}

この部分はNext.js 15からはあまり変わりませんでしたが、cacheComponentsで使用するとはるかに優れています。不透明なフレームワークレベルのキャッシュを無効化しようとするのではなく、特定のキャッシュコンポーネントをタグ付けしているからです。

Next.js 16 cacheComponents: App Routerキャッシングから91,000ページを移行 - アーキテクチャ

91,000ページの移行戦略

私たちはこれを一度にはしませんでした。14のロケールにわたる91,000ページで、大規模な一括移行は無謀だったでしょう。ここで私たちがそれをどのように分割したかです:

フェーズ1:Next.js 16へアップグレード、キャッシュ変更なし(1-2週目)

私たちはcacheComponentsを有効にすることなくNext.js 14.2からv16.0にアップグレードしました。これだけで動作を変更しました。フェッチリクエストがデフォルトでキャッシュされなくなったからです。期待したとおりTTFB回帰が発生しました:

  • 平均TTFBは180msから340msに増加しました(商品ページの場合)
  • オリジンサーバー負荷が〜60%増加しました(Sanity CDNは大丈夫でしたが、カスタムAPIエンドポイントはそうではありませんでした)
  • ISR再検証は実際に高速化しました。なぜなら管理するキャッシュ状態が少なくなったからです

これは私たちが疑っていたことを確認しました:私たちは暗黙的なキャッシングに大きく依存していて、多くのページは本当にキャッシングが必要でした。ただし、明示的で意図的なキャッシング。

フェーズ2:監査と分類(3週目)

私たちはアプリ内のすべてのルートを分類しました:

ページタイプ キャッシュ戦略 cacheLifeプロフィール
商品詳細ページ 42,000 商品タグでキャッシュ products(5分古い / 1時間再検証)
カテゴリリストページ 3,200 カテゴリタグでキャッシュ products(5分古い / 1時間再検証)
編集/ブログページ 8,400 積極的にキャッシュ editorial(1時間古い / 24時間再検証)
ローカライズ版 31,647 ベースページと同じ ベースページから継承
アカウント/動的ページ 6,000 キャッシュなし N/A

フェーズ3:cacheComponentsを有効化、ディレクティブを追加(4-6週目)

フラグを有効にし、"use cache"ディレクティブを追加し始めました。重要な決定:ほとんどのルートではページレベルでキャッシュしましたが、静的/動的コンテンツの混在があるページではコンポーネントレベルでキャッシュしました。

商品ページでは、商品情報と画像はキャッシュされていましたが、価格設定コンポーネントと在庫ステータスはキャッシュされていませんでした:

// components/ProductInfo.tsx
"use cache";

import { cacheLife, cacheTag } from 'next/cache';

export async function ProductInfo({ slug }: { slug: string }) {
  cacheLife('products');
  cacheTag(`product-${slug}`, 'product-info');
  
  const product = await getProduct(slug);
  
  return (
    <section>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <ProductImages images={product.images} />
    </section>
  );
}
// components/DynamicPricing.tsx
// "use cache"ディレクティブなし -- 常に新鮮

export async function DynamicPricing({ productId }: { productId: string }) {
  const pricing = await getPricing(productId); // 毎回価格設定APIにヒット
  
  return (
    <div className="pricing">
      <span className="price">${pricing.current}</span>
      {pricing.onSale && <span className="was-price">${pricing.original}</span>}
    </div>
  );
}

フェーズ4:ウェブフック統合(7週目)

正しいタグでrevalidateTagを呼び出すようにSanityウェブフックを再配線しました。これは実際には古いセットアップより簡単でした。なぜなら、タグは現在コード内で明示的で、フェッチオプション全体に散乱していないからです。

// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';

export async function POST(request: NextRequest) {
  const body = await request.json();
  const secret = request.headers.get('x-webhook-secret');
  
  if (secret !== process.env.REVALIDATION_SECRET) {
    return new Response('Unauthorized', { status: 401 });
  }

  switch (body._type) {
    case 'product':
      revalidateTag(`product-${body.slug.current}`);
      revalidateTag('product-listing');
      break;
    case 'category':
      revalidateTag(`category-${body.slug.current}`);
      revalidateTag('navigation');
      break;
    case 'article':
      revalidateTag(`article-${body.slug.current}`);
      break;
  }

  return new Response('OK', { status: 200 });
}

実装:ステップバイステップ

同様の移行を行っている場合、ここで実用的なプレイブックをお勧めします。(社会人動物園でのNext.js開発プロジェクトに対して現在使用しているもの)

ステップ1:フラグを有効化

// next.config.js
module.exports = {
  experimental: {
    cacheComponents: true,
    cacheLife: {
      // 理にかなったデフォルトから始めます
      default: {
        stale: 60,
        revalidate: 900,
        expire: 86400,
      },
    },
  },
};

ステップ2:ホットパスを見つける

アナリティクスを使用して、最も多くのトラフィックを得るページと、TTFBが最も重要な場所を特定します。私たちにとっては、カテゴリページ(高トラフィック、比較的安定したコンテンツ)と商品ページ(高トラフィック、適度に動的なコンテンツ)でした。

ステップ3:トップダウンで`"use cache"`を追加

レイアウトから始めます。ルートレイアウトがナビゲーションデータをフェッチしている場合は、最初にそれをキャッシュしてください。最も高い影響、最も低いリスク変更です:

// app/layout.tsx
// 注:レイアウト上の「use cache」はレイアウトシェルをキャッシュします
// 子ページは依然として独立してレンダリングされます

import { Navigation } from '@/components/Navigation';

export default async function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Navigation /> {/* このコンポーネントは独自の"use cache"を持っています */}
        {children}
      </body>
    </html>
  );
}

ステップ4:監視をセットアップ

Vercelの組み込みアナリティクスとカスタムログを使用してキャッシュヒット率を追跡しました。cacheComponentsを有効にした最初の週、キャッシュヒット率は34%でした。stale期間を調整した後、78%に上昇しました。

パフォーマンス結果とベンチマーク

Vercelのプロプラン上で30日間の測定により、完全な移行後の実数は以下の通りです:

メトリック 前(Next.js 14) 後フェーズ1(v16、キャッシュなし) 完全な移行後
平均TTFB(商品ページ) 180ms 340ms 95ms
平均TTFB(カテゴリページ) 220ms 410ms 72ms
平均TTFB(編集ページ) 150ms 280ms 45ms
P99 TTFB(すべてのページ) 1,200ms 2,100ms 380ms
ビルド時間(フル) 3時間12分 2時間48分 48分
Vercel関数呼び出し/日 2.4M 3.8M 1.1M
月額Vercel請求 〜$840 〜$1,200 〜$520
キャッシュヒット率 不明(暗黙) N/A 78%
古いコンテンツインシデント(#cache-crimes) 8-12/週 0 1-2/月

ビルド時間の改善は説明に値する。cacheComponentsでは、ビルド時にすべての91,000ページを生成することから離れました。代わりに、トップ5,000ページだけを静的に生成し、残りはオンデマンド生成でキャッシングされました。cacheComponentsディレクティブは、最初の訪問後のオンデマンドページがキャッシュされたことを意味していました。cacheLifeがstale性を制御していました。

Vercel請求額の低下は重要でした。より少ない関数呼び出し(明示的なコンポーネントキャッシングのため)とより短いビルド時間は、実際のコスト削減を意味していました。その〜$320/月の削減はそれ自体を支払います。

落とし穴と注意点

シリアル化境界

"use cache"ディレクティブはシリアル化境界を作成します。キャッシュされたコンポーネントにプロップとして渡されるすべてはシリアル化可能である必要があります。プロップとしてコールバック関数またはReact要素を受け取っていた複数のコンポーネントがありました。これらはすぐに壊れました。修正は、代わりにコンポジション パターンを使用するように再構成することでした:

// ❌ "use cache"で壊れます
"use cache";
export async function ProductCard({ product, onAddToCart }) {
  // onAddToCartは関数です -- シリアル化不可!
}

// ✅ これは機能します
"use cache";
export async function ProductCard({ product }) {
  return (
    <div>
      <h2>{product.name}</h2>
      {/* AddToCartはClient Componentです、キャッシュされません */}
      <AddToCartButton productId={product.id} />
    </div>
  );
}

動的パラメータとキャッシュキー爆発

91,000ページで、それぞれ一意のパラメータを持つ、キャッシュキー空間は膨大です。最初の週にVercelのエッジキャッシュ制限にヒットし、どのページが長いexpire値を取得するかについて、より戦略的である必要がありました。低トラフィックロングテールページは短いキャッシュ期間を取得しました。

`Date.now()`トラップ

"use cache"を使用し、キャッシュされた関数の内側でDate.now()またはnew Date()を呼び出すコンポーネントは、そのタイムスタンプをキャッシュします。私たちはこれを数時間同じ時刻を表示する「最後に更新」ディスプレイで見つけました。修正:時間に敏感なロジックをClient ComponentまたはキャッシュされていないServer Componentに移動させます。

ネストされたキャッシュ境界

他のキャッシュされたコンポーネント内にキャッシュされたコンポーネントをネストすると、内部キャッシュは独自のライフサイクルを持ちます。これは強力ですが、紛らわしいです。チームコンベンション:ページレベルまたはコンポーネントレベルでキャッシュしますが、明確な理由がない限り両方ではありません。

cacheComponentsを使うべき場合と使うべきでない場合

使用する場合:

  • 数百ページより多くあり、ISRビルド時間が困っています
  • コンテンツに、セクション全体で異なるフレッシュネス要件があります
  • キャッシュされるもの対常に新鮮なものについての粒状制御が必要です
  • Vercelまたはそれ以外のプラットフォームで実行しており、Next.jsキャッシュレイヤーをサポートしています
  • 高トラフィックサイトのインフラストラクチャコストを削減したいです

使用しない場合:

  • サイトが十分に小さく、完全なSSGが正常に機能します
  • すべてのページが完全に動的です(すべての場所にユーザー固有のコンテンツ)
  • Next.jsキャッシングインフラストラクチャをサポートするホスティングプラットフォームにいません
  • チームがNext.jsに新しい場合、最初に基本に慣れています

プロジェクトがこのレベルのキャッシング制御が必要かどうかを評価している場合、またはAstroのような異なるフレームワークがコンテンツ満載のサイトにより適しているかもしれない場合、移行にコミットする前に考える価値があります。

複数のヘッドレスCMSソースからコンテンツが来るプロジェクトでは、Next.js 16のcacheTagシステムはヘッドレスCMSアーキテクチャでうまく機能します。各コンテンツタイプは独自の無効化チャネルを取得します。

FAQ

Next.js 16のcacheComponentsとは何ですか?

cacheComponentsはNext.js 16の実験的な設定オプションであり、Server Componentsの"use cache"ディレクティブを有効にします。どのコンポーネントをキャッシュする必要があるか、そしてcacheLifeを使用してカスタムキャッシュプロフィールを定義するかを明示的に標識できます。これはNext.js 15で実験的だったuse cacheディレクティブの安定した進化です。

cacheComponentsはISR(段階的な静的再生成)とどのように異なりますか?

ISRはページ全体をキャッシュし、時間ベースのスケジュールで再検証します。cacheComponentsを使用すると、ページ内の個々のコンポーネントをキャッシュでき、それぞれ異なるキャッシュ有効期限があります。単一ページは、ヘッダーを24時間キャッシュ、商品情報を1時間キャッシュ、価格設定は決してキャッシュしないことができます。ISRはそれをすることはできません。すべてまたはページレベルでは何もできません。

cacheComponentsを使用するためにVercelにいる必要がありますか?

いいえですが、キャッシングインフラストラクチャが緊密に統合されているため、Vercelでの経験は最高です。自己ホスト型Next.jsデプロイメントはファイルシステムキャッシュアダプターでcacheComponentsを使用できますが、エッジ配布利点は取得しません。NetlifyやCloudflareなどのプラットフォームはサポートを追加しており、中盤2026年現在、Vercelは最も完全な実装のままです。

Next.js 16でキャッシュされたコンポーネントを無効化するにはどうすればよいですか?

キャッシュされたコンポーネント内でcacheTag()を使用してタグを割り当てます。その後、サーバーアクション、ルートハンドラー、またはウェブフックエンドポイントからrevalidateTag('tag-name')を呼び出します。これにより、そのタグを持つすべてのキャッシュされたコンポーネントが無効化されます。これはNext.js 15からの同じAPIですが、不透明なフレームワークレベルのキャッシュを無効化しようとするのではなく、明示的なキャッシュコンポーネントをタグ付けしているため、現在ははるかに有用です。

cacheComponentsはVercel請求書を削減しますか?

かなり削減できます。当社の場合、関数呼び出しが54%削減されました。サーバーレス関数を呼び出すのではなく、キャッシュレイヤーからキャッシュされたコンポーネント応答が提供されたからです。ビルド時間削減もビルド分に保存します。あなたの実走行距離は、トラフィックパターンとキャッシュヒット率に基づいて異なります。Vercelの価格計算ツールで現在の使用方法を確認してください。

シリアル化不可能なプロップを受け取るコンポーネントに「use cache」を追加した場合、どうなりますか?

ビルドエラーが発生します。"use cache"ディレクティブはシリアル化境界を作成するため、すべてのプロップはシリアル化可能である必要があります(文字列、数字、プレーンオブジェクト、配列)。関数、React要素、クラスインスタンス、およびその他のシリアル化不可能な値はビルドを失敗させます。データプロップのみを受け入れるようにコンポーネントを再構成し、子Client Componentsで相互作用を処理します。

他のフレームワークのReact Server ComponentsでcacheComponentsを使用できますか? いいえ。cacheComponentsはReact Server Componentsの上に構築される次.js固有の機能です。"use cache"ディレクティブ構文は最終的にReact標準になるかもしれませんが、cacheLifeプロフィールとcacheTagシステムはNext.js APIです。RemixやカスタムRSCセットアップなどのフレームワークを使用している場合、異なるキャッシング戦略が必要になります。

大規模なNext.jsサイトをcacheComponentsに移行するのにどのくらい時間がかかりますか? 91,000ページのサイトを持つ4人開発者チームの場合、テストと監視を含む完全な移行には7週間かかりました。より小さなサイト(10,000ページ以下)で、より単純なデータモデルを使用すると、おそらく1-2週間でそれを行うことができます。実際のコード変更は簡単です。時間はキャッシングニーズを監査し、無効化フローをテストし、デプロイ後のキャッシュヒット率を監視することに費やします。単独で行きたくない場合は、お問い合わせください。私たちはこれを何度もしてきました。