Next.js 16 の Cache Components 完全ガイド:use cache と PPR で変わるキャッシュ戦略

Next.js 16 で導入された Cache Components を徹底解説。use cache ディレクティブの3つの適用レベル、cacheLife によるキャッシュ寿命の制御、cacheTag と updateTag/revalidateTag による無効化、PPR との統合まで実践コード付きで紹介します。

はじめに:Next.js のキャッシュが生まれ変わった

Next.js App Router が登場してから、キャッシュの挙動って開発者にとって最大の頭痛の種でしたよね。fetch が自動的にキャッシュされるのか、されないのか、どのタイミングで無効化されるのか——正直、この暗黙的なキャッシュモデルには筆者もかなり振り回されました。

Next.js 16 はこの問題に真正面から取り組んで、キャッシュの仕組みを根本から作り直しています。

新しい Cache Components モデルでは、すべてのページがデフォルトで動的になり、キャッシュは完全にオプトイン方式に変わります。その中心にあるのが "use cache" ディレクティブです。React の "use client""use server" と同じ感覚で書けるので、既存の知識がそのまま活きるのが嬉しいところですね。

この記事では、Cache Components と Partial Prerendering(PPR)の仕組みを基礎から実践まで掘り下げていきます。具体的なコード例をたっぷり入れたので、手を動かしながら読んでもらえると理解が早いはずです。

なぜキャッシュモデルが変わったのか

旧モデルの問題点

Next.js 14 以前の App Router では、fetch リクエストがデフォルトでキャッシュされていました。一見パフォーマンスに有利に見えるんですが、実際に使ってみると結構やっかいな問題が出てくるんですよね。

  • 予測不能な挙動:開発者が意図せずキャッシュされたデータが表示されて、バグの原因になる
  • デバッグの困難さ:キャッシュが有効かどうかを確認するのがとにかく難しい
  • 暗黙的なルール:どの API がキャッシュされ、どれが動的レンダリングされるかのルールが複雑すぎる
  • 一括切り替え:ルート全体が静的か動的かの二択で、部分的な制御ができない

特に3番目の「暗黙的なルール」は厄介でした。cookies() を呼ぶと動的になるけど、fetch はキャッシュされる……みたいな暗黙のルールを全部覚えるのは、正直無理がありました。

Next.js 16 の新しいアプローチ

Next.js 16 では、デフォルトの挙動が180度変わりました。

  • 全ページがデフォルトで動的:明示的にキャッシュを指定しない限り、すべてのリクエストは毎回サーバーで処理される
  • オプトインキャッシュ"use cache" ディレクティブを使って、キャッシュしたい部分だけを明示的に指定する
  • コンポーネントレベルの制御:ページ全体ではなく、個々のコンポーネントや関数単位でキャッシュ戦略を設定できる

この変更、個人的にはかなり歓迎しています。「キャッシュされてないと思ったらされてた」というバグがなくなるだけで、開発体験が段違いに良くなります。

Cache Components のセットアップ

まずは設定から。Cache Components を使うには、next.config.tscacheComponents フラグを有効にします。

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

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig

たったこれだけです。この設定で "use cache" ディレクティブ、cacheLifecacheTag などの API が全部使えるようになります。ちなみに、以前の experimental.ppr 設定はもう不要で、cacheComponents がその役割を引き継いでいます。

"use cache" ディレクティブの3つのレベル

"use cache" ディレクティブは、非同期関数やコンポーネントの戻り値をキャッシュします。適用できるレベルは3つ。引数や親スコープからクロージャで取得される値が自動的にキャッシュキーの一部になるので、異なる入力に対しては個別のキャッシュエントリが生成されます(ここ地味に重要です)。

1. ファイルレベル

ファイルの先頭に "use cache" を置くと、そのファイル内のすべてのエクスポートがキャッシュ対象になります。

// app/blog/page.tsx
"use cache"

import { cacheLife } from "next/cache"

export default async function BlogPage() {
  cacheLife("hours")
  const posts = await fetchPosts()

  return (
    <div>
      <h1>ブログ記事一覧</h1>
      {posts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

ファイルレベルの "use cache" は、ページ全体を静的にキャッシュしたいときに最適です。コンパイラがルートと動的セグメントに基づいてキャッシュキーを自動生成してくれます。

2. コンポーネントレベル

特定のコンポーネントだけキャッシュしたい場合は、コンポーネント関数の本体の先頭にディレクティブを配置します。

// components/ProductCard.tsx
import { cacheLife, cacheTag } from "next/cache"

async function ProductCard({ productId }: { productId: string }) {
  "use cache"
  cacheLife("days")
  cacheTag("products", `product-${productId}`)

  const product = await db.products.findUnique({
    where: { id: productId },
  })

  return (
    <div className="product-card">
      <h3>{product.name}</h3>
      <p>¥{product.price.toLocaleString()}</p>
      <p>{product.description}</p>
    </div>
  )
}

export default ProductCard

これが Cache Components の一番おいしいところです。同じページ内で異なるキャッシュ戦略を混在させられるんですよね。頻繁に更新される部分はキャッシュなし、安定したデータはキャッシュあり、という使い分けが自然にできます。

3. 関数レベル

データ取得関数やビジネスロジック関数を個別にキャッシュすることもできます。

// lib/data.ts
import { cacheTag, cacheLife } from "next/cache"

export async function getCategories() {
  "use cache"
  cacheLife("weeks")
  cacheTag("categories")

  return await db.categories.findMany({
    orderBy: { sortOrder: "asc" },
  })
}

export async function getProductsByCategory(categoryId: string) {
  "use cache"
  cacheLife("hours")
  cacheTag("products", `category-${categoryId}`)

  return await db.products.findMany({
    where: { categoryId },
    orderBy: { createdAt: "desc" },
  })
}

関数レベルのキャッシュは、同じデータ取得ロジックを複数のコンポーネントから呼び出すケースで特に威力を発揮します。キャッシュがリクエスト間で共有されるので、同じ引数なら重複した DB クエリを回避できるわけです。

cacheLife でキャッシュの寿命を制御する

cacheLife は、キャッシュの有効期限を設定する関数です。"use cache" とセットで使います。

組み込みプリセット

Next.js には以下の組み込みプリセットが用意されています。直感的なネーミングなので覚えやすいですね。

  • "seconds":数秒間のキャッシュ(リアルタイムに近いデータ向け)
  • "minutes":数分間のキャッシュ(頻繁に更新されるデータ向け)
  • "hours":数時間のキャッシュ(1日に数回更新されるデータ向け)
  • "days":数日間のキャッシュ(比較的安定したデータ向け)
  • "weeks":数週間のキャッシュ(ほとんど変わらないデータ向け)
  • "max":最大期間のキャッシュ(静的コンテンツ向け)

カスタムプロファイルの定義

組み込みプリセットでは足りないケースもありますよね。そういうときは next.config.ts でカスタムプロファイルを定義できます。

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

const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheLife: {
    // 商品データ用:30分で再検証、1時間で期限切れ
    productData: {
      stale: 60 * 30,        // 30分間はstaleデータを返す
      revalidate: 60 * 15,   // 15分ごとにバックグラウンドで再検証
      expire: 60 * 60,       // 1時間で完全に期限切れ
    },
    // ユーザープロフィール用:5分で再検証
    userProfile: {
      stale: 60 * 5,
      revalidate: 60 * 2,
      expire: 60 * 10,
    },
    // CMS コンテンツ用:1日キャッシュ
    cmsContent: {
      stale: 60 * 60 * 24,
      revalidate: 60 * 60,
      expire: 60 * 60 * 24 * 7,
    },
  },
}

export default nextConfig

定義したプロファイルは、アプリ全体で名前で呼び出せます。

import { cacheLife } from "next/cache"

export async function getProduct(id: string) {
  "use cache"
  cacheLife("productData")  // カスタムプロファイルを使用
  return await db.products.findUnique({ where: { id } })
}

stale・revalidate・expire の違い

カスタムプロファイルの3つのパラメータ、最初はちょっと混乱するかもしれません。それぞれの役割を整理しておきます。

  • stale:この期間内はキャッシュされたデータをそのまま返す(クライアントキャッシュの寿命)
  • revalidate:この間隔でサーバー側がバックグラウンド再検証を実行する
  • expire:キャッシュエントリが完全に削除されるまでの最大期間

要するに HTTP キャッシュヘッダの stale-while-revalidate パターンと同じ考え方です。古いデータを即座に返しつつ、裏で新しいデータを取ってくるので、ユーザー体験を損なわずにデータの鮮度を保てます。この仕組み、地味ですがめちゃくちゃ実用的です。

cacheTag によるタグベースのキャッシュ管理

cacheTag はキャッシュエントリにタグを付けて、オンデマンドでの無効化を可能にする仕組みです。

基本的な使い方

import { cacheTag } from "next/cache"

export async function getBlogPosts() {
  "use cache"
  cacheTag("blog-posts")

  return await db.posts.findMany({
    where: { status: "published" },
    orderBy: { publishedAt: "desc" },
  })
}

export async function getBlogPost(slug: string) {
  "use cache"
  cacheTag("blog-posts", `blog-post-${slug}`)

  return await db.posts.findUnique({
    where: { slug },
  })
}

タグの設計パターン

キャッシュ無効化を効率的に行うには、タグの設計がかなり重要です。以下のパターンがおすすめです。

  • コレクションタグ"products""blog-posts" — コレクション全体を一括無効化
  • 個別タグ`product-${id}``post-${slug}` — 特定のアイテムだけをピンポイントで無効化
  • カテゴリタグ`category-${categoryId}` — カテゴリに属するアイテムをまとめて無効化
  • ユーザータグ`user-${userId}` — 特定ユーザーに関連するデータを無効化

1つのキャッシュエントリに複数のタグを付けることで、色々な粒度で無効化を制御できます。ちなみにタグの最大長は256文字、1エントリあたり最大128個まで設定可能です。

キャッシュの無効化:revalidateTag と updateTag

キャッシュを無効化する方法は2つあります。それぞれ得意なユースケースが違うので、使い分けが大事です。

revalidateTag:バックグラウンドでの再検証

revalidateTag は、いわゆる Stale-While-Revalidate(SWR)パターンに基づいた無効化です。タグを無効化しても、ユーザーにはまずキャッシュされたデータを返して、裏側で新しいデータを取得します。

import { revalidateTag } from "next/cache"

// Server Action または Route Handler で使用
export async function refreshBlogPosts() {
  // 組み込みプロファイルを指定(推奨は "max")
  revalidateTag("blog-posts", "max")
}

// カスタム有効期限を指定することも可能
export async function refreshProducts() {
  revalidateTag("products", { expire: 3600 })
}

revalidateTag は Server Actions と Route Handlers の両方で使えます。「多少古いデータが見えても問題ない」ケースに向いています。

updateTag:即座の無効化

updateTag は Server Actions 専用の即時無効化 API です。キャッシュを即座に期限切れにして、次のリクエストで確実に最新データを返します。

"use server"

import { updateTag } from "next/cache"
import { redirect } from "next/navigation"

export async function createBlogPost(formData: FormData) {
  const title = formData.get("title") as string
  const content = formData.get("content") as string

  // データベースに新しい記事を作成
  const post = await db.posts.create({
    data: { title, content, status: "published" },
  })

  // キャッシュを即座に無効化
  updateTag("blog-posts")
  updateTag(`blog-post-${post.slug}`)

  // 新しい記事ページにリダイレクト
  redirect(`/blog/${post.slug}`)
}

revalidateTag と updateTag の使い分け

特性 revalidateTag updateTag
使用場所 Server Actions・Route Handlers Server Actions のみ
無効化方式 バックグラウンドで再検証(SWR) 即座にキャッシュを期限切れ
ユースケース 最終的な一貫性で十分な場合 自分の書き込みを即座に反映したい場合
ユーザー体験 古いデータが一時的に表示される 最新データの取得を待つ

ざっくり言うと、ユーザーが自分で操作した結果を即座に見たい場面(記事の作成・編集・削除など)には updateTag、外部システムからの更新通知(Webhook など)には revalidateTag。実務ではこの使い分けでほぼカバーできます。

Partial Prerendering(PPR)と Cache Components の統合

Partial Prerendering は、1つのルート内で静的な部分と動的な部分を混在させる機能です。以前は実験的な扱いでしたが、Next.js 16 で Cache Components と統合され、cacheComponents: true を設定するだけで使えるようになりました。

PPR の仕組み

PPR の動作フローは以下の通りです。

  1. ビルド時"use cache" が指定された部分が事前レンダリングされ、静的シェルとして保存される
  2. リクエスト時:静的シェルが即座に返され、動的な部分は Suspense の fallback として表示される
  3. ストリーミング:動的な部分がサーバーで処理され次第、クライアントにストリーミングされる

つまり、ユーザーは静的な「ガワ」を爆速で受け取りつつ、動的な部分は後から埋まっていくわけです。体感速度が全然違います。

実践的なダッシュボードの例

では、静的・キャッシュ・動的な部分を組み合わせたダッシュボードの実装を見てみましょう。

// app/dashboard/page.tsx
import { Suspense } from "react"
import { cookies } from "next/headers"
import Stats from "@/components/Stats"
import Notifications from "@/components/Notifications"
import RecentActivity from "@/components/RecentActivity"
import { StatsSkeleton, NotificationsSkeleton, ActivitySkeleton } from "@/components/skeletons"

export default function DashboardPage() {
  return (
    <div className="dashboard">
      {/* 静的シェル — CDN から即座に配信 */}
      <header>
        <h1>ダッシュボード</h1>
        <nav>{/* ナビゲーション */}</nav>
      </header>

      <div className="dashboard-grid">
        {/* キャッシュされた統計情報 — 1時間ごとに再検証 */}
        <Suspense fallback={<StatsSkeleton />}>
          <Stats />
        </Suspense>

        {/* 動的な通知 — 毎回最新データを取得 */}
        <Suspense fallback={<NotificationsSkeleton />}>
          <Notifications />
        </Suspense>

        {/* キャッシュされた最近のアクティビティ — 5分ごとに再検証 */}
        <Suspense fallback={<ActivitySkeleton />}>
          <RecentActivity />
        </Suspense>
      </div>
    </div>
  )
}
// components/Stats.tsx
import { cacheLife, cacheTag } from "next/cache"

export default async function Stats() {
  "use cache"
  cacheLife("hours")
  cacheTag("dashboard-stats")

  const [totalUsers, totalOrders, revenue] = await Promise.all([
    db.users.count(),
    db.orders.count({ where: { status: "completed" } }),
    db.orders.aggregate({ _sum: { total: true } }),
  ])

  return (
    <div className="stats-grid">
      <div className="stat-card">
        <h3>ユーザー数</h3>
        <p>{totalUsers.toLocaleString()}</p>
      </div>
      <div className="stat-card">
        <h3>注文数</h3>
        <p>{totalOrders.toLocaleString()}</p>
      </div>
      <div className="stat-card">
        <h3>売上</h3>
        <p>¥{revenue._sum.total?.toLocaleString()}</p>
      </div>
    </div>
  )
}
// components/Notifications.tsx
import { cookies } from "next/headers"

export default async function Notifications() {
  // "use cache" なし — 毎回リクエスト時に実行される
  const userId = (await cookies()).get("userId")?.value

  if (!userId) return null

  const notifications = await db.notifications.findMany({
    where: { userId, read: false },
    orderBy: { createdAt: "desc" },
    take: 10,
  })

  return (
    <div className="notifications">
      <h3>未読の通知({notifications.length}件)</h3>
      <ul>
        {notifications.map((n) => (
          <li key={n.id}>{n.message}</li>
        ))}
      </ul>
    </div>
  )
}
// components/RecentActivity.tsx
import { cacheLife, cacheTag } from "next/cache"

export default async function RecentActivity() {
  "use cache"
  cacheLife("minutes")
  cacheTag("recent-activity")

  const activities = await db.activityLog.findMany({
    orderBy: { createdAt: "desc" },
    take: 20,
    include: { user: true },
  })

  return (
    <div className="recent-activity">
      <h3>最近のアクティビティ</h3>
      <ul>
        {activities.map((activity) => (
          <li key={activity.id}>
            <span>{activity.user.name}</span>
            <span>{activity.description}</span>
            <time>{new Date(activity.createdAt).toLocaleString("ja-JP")}</time>
          </li>
        ))}
      </ul>
    </div>
  )
}

このダッシュボードでは、3つの異なるキャッシュ戦略が1ページ内で共存しています。ヘッダーは完全に静的、統計情報は1時間キャッシュ、アクティビティは数分キャッシュ、そして通知はリアルタイム取得。これが Cache Components + PPR の真骨頂です。

ネストされた use cache ディレクティブの挙動

ここ、ちょっとハマりポイントなので丁寧に説明します。

キャッシュされたコンポーネントの中に、別のキャッシュされた関数やコンポーネントがある場合、それぞれの cacheLife は独立して動作します。

// 外側のコンポーネント
async function ProductPage({ id }: { id: string }) {
  "use cache"
  cacheLife("hours")  // 1時間キャッシュ

  const product = await getProduct(id) // 内側もキャッシュされている

  return (
    <div>
      <h1>{product.name}</h1>
      <ProductReviews productId={id} />
    </div>
  )
}

// 内側の関数
async function getProduct(id: string) {
  "use cache"
  cacheLife("days")  // 数日間キャッシュ
  cacheTag(`product-${id}`)

  return await db.products.findUnique({ where: { id } })
}

ここで覚えておきたいのは、外側のキャッシュがヒットした場合、内側の関数はそもそも呼ばれないということ。外側がキャッシュを保存した時点の結果がまるっと返されます。内側の cacheLife("days") が活きるのは、外側のキャッシュが切れて再実行されたときだけです。

EC サイトでの実践パターン

ここからは実践編です。EC サイトを例に、Cache Components のリアルな活用パターンを見ていきましょう。

商品一覧ページ

// app/products/page.tsx
import { Suspense } from "react"
import { cacheLife, cacheTag } from "next/cache"

// カテゴリナビゲーション — 週単位でキャッシュ
async function CategoryNav() {
  "use cache"
  cacheLife("weeks")
  cacheTag("categories")

  const categories = await db.categories.findMany()

  return (
    <nav>
      {categories.map((cat) => (
        <a key={cat.id} href={`/products?category=${cat.slug}`}>
          {cat.name}
        </a>
      ))}
    </nav>
  )
}

// 商品リスト — 1時間キャッシュ
async function ProductList({ category }: { category?: string }) {
  "use cache"
  cacheLife("hours")
  cacheTag("products", category ? `category-${category}` : "all-products")

  const products = await db.products.findMany({
    where: category ? { category: { slug: category } } : {},
    orderBy: { createdAt: "desc" },
    take: 20,
  })

  return (
    <div className="product-grid">
      {products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  )
}

// 在庫情報 — リアルタイム(キャッシュなし)
async function StockBadge({ productId }: { productId: string }) {
  const stock = await db.inventory.findUnique({
    where: { productId },
  })

  return (
    <span className={stock?.quantity > 0 ? "in-stock" : "out-of-stock"}>
      {stock?.quantity > 0 ? "在庫あり" : "在庫切れ"}
    </span>
  )
}

カテゴリは週単位、商品リストは時間単位、在庫はリアルタイム。データの性質に合わせてキャッシュ戦略を変えるのがポイントです。

Server Action での在庫更新とキャッシュ無効化

// actions/order.ts
"use server"

import { updateTag } from "next/cache"
import { redirect } from "next/navigation"

export async function placeOrder(formData: FormData) {
  const productId = formData.get("productId") as string
  const quantity = Number(formData.get("quantity"))

  // トランザクションで注文を処理
  const order = await db.$transaction(async (tx) => {
    // 在庫を減らす
    await tx.inventory.update({
      where: { productId },
      data: { quantity: { decrement: quantity } },
    })

    // 注文を作成
    return await tx.orders.create({
      data: {
        productId,
        quantity,
        status: "pending",
      },
    })
  })

  // 関連するキャッシュを即座に無効化
  updateTag("products")
  updateTag(`product-${productId}`)
  updateTag("dashboard-stats")

  redirect(`/orders/${order.id}`)
}

注文が入ったら updateTag で即座にキャッシュを無効化。ユーザーがリダイレクト先のページを見たときには、最新の在庫状況が反映されています。

Webhook での外部更新とキャッシュ無効化

CMS や決済サービスなど、外部システムからの Webhook で更新を受け取るパターンもよくありますよね。

// app/api/webhook/cms/route.ts
import { revalidateTag } from "next/cache"
import { NextRequest, NextResponse } from "next/server"

export async function POST(request: NextRequest) {
  // Webhook の認証を検証
  const signature = request.headers.get("x-webhook-signature")
  if (!verifySignature(signature, await request.text())) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
  }

  const payload = await request.json()

  switch (payload.event) {
    case "content.published":
      // CMS コンテンツが公開された場合
      revalidateTag("blog-posts", "max")
      revalidateTag(`blog-post-${payload.data.slug}`, "max")
      break

    case "content.deleted":
      // 即座にキャッシュを期限切れにする
      revalidateTag(`blog-post-${payload.data.slug}`, { expire: 0 })
      revalidateTag("blog-posts", { expire: 0 })
      break
  }

  return NextResponse.json({ success: true })
}

注意点として、Route Handlers では updateTag は使えません。即座にキャッシュを消したい場合は revalidateTag{ expire: 0 } を渡すのがコツです。

移行ガイド:旧キャッシュモデルからの移行

既存プロジェクトを Cache Components に移行する手順を、ステップバイステップで紹介します。一度に全部やろうとせず、段階的に進めるのがおすすめです。

ステップ 1:cacheComponents を有効化

// next.config.ts
const nextConfig = {
  cacheComponents: true,
  // experimental.ppr は削除
}

ステップ 2:暗黙的キャッシュを明示的に置き換え

以前の fetch ベースのキャッシュ設定を "use cache" に書き換えていきます。

// 旧:fetch の revalidate オプション
const data = await fetch("https://api.example.com/data", {
  next: { revalidate: 3600 },
})

// 新:use cache + cacheLife
async function getData() {
  "use cache"
  cacheLife("hours")
  const res = await fetch("https://api.example.com/data")
  return res.json()
}

ステップ 3:ルートセグメント設定を置き換え

// 旧:ルートセグメント設定
export const revalidate = 3600
export const dynamic = "force-static"

// 新:ファイルレベルの use cache
"use cache"
import { cacheLife } from "next/cache"

export default async function Page() {
  cacheLife("hours")
  // ...
}

ステップ 4:generateStaticParams との併用

generateStaticParams は引き続き使えます。Cache Components と組み合わせれば、ビルド時に静的パスを生成しつつ、コンポーネントレベルでキャッシュを細かく制御できます。

// app/blog/[slug]/page.tsx
"use cache"

import { cacheLife } from "next/cache"

export async function generateStaticParams() {
  const posts = await db.posts.findMany({ select: { slug: true } })
  return posts.map((post) => ({ slug: post.slug }))
}

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  cacheLife("days")
  const { slug } = await params
  const post = await db.posts.findUnique({ where: { slug } })

  return (
    <article>
      <h1>{post?.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post?.content ?? "" }} />
    </article>
  )
}

パフォーマンスのベストプラクティス

最後に、Cache Components を最大限に活かすためのポイントをまとめておきます。

1. キャッシュの粒度を適切に設定する

ページ全体をキャッシュするのではなく、データの更新頻度に応じてコンポーネント単位でキャッシュを設定しましょう。こうすることで、一部のデータが変わっただけでページ全体のキャッシュが吹き飛ぶ、という事態を防げます。

2. タグの命名規則を統一する

チーム開発では特に大事です。コレクション名は複数形("products")、個別タグはハイフン区切り(`product-${id}`)のように、ルールを決めてドキュメント化しておきましょう。後から「このタグなんだっけ?」となるのを防げます。

3. Suspense バウンダリを戦略的に配置する

PPR の効果を最大化するには、Suspense の配置がカギです。キャッシュされたコンポーネントと動的なコンポーネントの境界に、意味のある fallback UI(スケルトンなど)を置くことで、ユーザーに最速の初期表示を提供できます。

4. 並列データフェッチを活用する

キャッシュされた関数でも、複数の独立したデータ取得は Promise.all で並列実行すべきです。キャッシュミス時のパフォーマンスが全然違います。

async function DashboardStats() {
  "use cache"
  cacheLife("hours")

  // 並列でデータを取得
  const [users, orders, revenue] = await Promise.all([
    db.users.count(),
    db.orders.count(),
    db.orders.aggregate({ _sum: { total: true } }),
  ])

  return <StatsDisplay users={users} orders={orders} revenue={revenue} />
}

5. 開発中のキャッシュデバッグ

開発環境でキャッシュの挙動を確認したいときは、Next.js のログ設定を有効にしておくと便利です。

// next.config.ts
const nextConfig = {
  cacheComponents: true,
  logging: {
    fetches: {
      fullUrl: true,  // フェッチ URL をフル表示
      hmrRefreshes: true,  // HMR 時のフェッチも表示
    },
  },
}

これで開発サーバーのコンソールにキャッシュのヒット・ミスが表示されるようになります。「なんでこのデータ古いの?」と悩む時間を大幅に減らせるはずです。

まとめ

Next.js 16 の Cache Components は、これまでのキャッシュの「わかりにくさ」を一掃してくれる、待望のアップデートです。

重要なポイントを振り返ります。

  • デフォルトは動的:すべてのページはデフォルトで動的レンダリング。キャッシュは明示的にオプトイン
  • 3つのレベル:ファイル、コンポーネント、関数レベルで "use cache" を適用可能
  • cacheLife:組み込みプリセットやカスタムプロファイルでキャッシュ寿命を柔軟に制御
  • cacheTag:タグベースのきめ細かなキャッシュ無効化
  • updateTag と revalidateTag:ユースケースに応じた2つの無効化戦略
  • PPR との統合:1つのルート内で静的・キャッシュ・動的コンテンツを自由に組み合わせ

新しいプロジェクトなら最初から Cache Components を使いましょう。既存プロジェクトの場合は、まずデータ取得関数に "use cache" を入れるところから始めて、徐々にコンポーネントレベルに広げていくのが安全な移行パスです。

正直、この新しいキャッシュモデルに慣れたら、もう旧モデルには戻れないと思います。

著者について Editorial Team

Our team of expert writers and editors.