🎉

Next.js 16で導入されたキャッシュコンポーネントについて理解する

に公開

はじめまして、_minoです!

この記事では、Next.js 16のキャッシュコンポーネントおよびキャッシュ周りについてキャッチアップした内容をまとめました!

まだまだ試行錯誤中ですが、案件で実際に検証したり、ドキュメントを読み込んだりしてまとめたので、学習の参考になれば幸いです🙌

🔥 キャッシュコンポーネントとは

Next.js 16で新しく導入されたCache Componentsは、ページレンダリングとキャッシュの新しいアプローチです。 PPR(Partial Prerendering:部分的事前レンダリング)を実装する機能として、「静的ページの速さ」と「動的データの鮮度」を両立させることができます。

PPRの説明はこちらの動画がかなりわかりやすかったのでおすすめです👇
https://www.youtube.com/watch?v=MTcPrTIBkpA

従来は、ページ全体を静的にするか動的にするかの二択でしたが、Cache Componentsではページの一部だけを静的にキャッシュし、残りを動的にレンダリングすることが可能になりました。

動作の仕組み


公式から抜粋したECサイトの商品ページの例

Navbar(ナビゲーション)とProduct Information(商品情報)は静的部分(紫色) として、ページを開いた瞬間に即座に表示されます。一方、Cart(カート)とRecommended(おすすめ商品)は動的部分(青色) として、データの準備ができ次第、順次表示されていきます。

これにより、ユーザーは待ち時間なく商品情報を閲覧でき、カートやおすすめ商品などの個人に紐づく情報も最小限の遅延で表示されます。

実装イメージ

import { Suspense } from 'react'

export default function ProductPage() {
  return (
    <>
      {/* 静的部分:即座に表示 */}
      <Navbar />
      <ProductInformation />
      
      {/* 動的部分:Suspenseでラップしてストリーミング */}
      <Suspense fallback={<div>Loading...</div>}>
        <Cart />
      </Suspense>
      
      <Suspense fallback={<div>Loading...</div>}>
        <Recommended />
      </Suspense>
    </>
  )
}

📝 従来のバージョンとの比較

Cache Componentsの登場でNext.js 15以前とNext.js 16では大きく設計が変わりました。

項目 Next.js 15以前 Next.js 16
デフォルトの動作 静的(自動でキャッシュ) 動的(毎回最新データを取得)
設定単位 ページ・レイアウト単位 関数・コンポーネント単位
キャッシュの種類 1種類 3種類(静的・動的・ユーザー固有)
キャッシュ対象 主にfetch データベース・API・ファイル等すべて
Suspenseの必要性 オプション 動的データには必須

Next.js 15から16へのアップグレードガイド、改善機能、破壊的変更については、こちらから確認できます👇
https://nextjs.org/docs/app/guides/upgrading/version-16

next devとnext buildを同時実行できるようになったのが、個人的に嬉しいです!

📚 3種類のキャッシュディレクティブ

Cache Componentsの登場に伴い、キャッシュディレクティブが3種類登場しました。
データの性質に応じて使い分けることで、最適なパフォーマンスを実現することができるようになります。

use cache

ビルド時にキャッシュされる静的な共有データに使用します。 従来の静的生成(SSG)に最も近い動作で、全ユーザーで同じ結果を共有します。

https://nextjs.org/docs/app/api-reference/directives/use-cache

✍️ 使い方
ブログ記事一覧のように、ビルド時に取得して全ユーザーで共有できるデータに最適です。

async function getBlogPosts() {
  'use cache'

  const posts = await fetch('https://blog.example.com/api/posts')
  return posts
}

use cache: remote

動的コンテキストで実行される、全ユーザーで共有できるデータのキャッシュに使用します。 cookies()connection()を呼び出した後など、通常のuse cacheが動作しない場面でもキャッシュを有効にできます。

https://nextjs.org/docs/app/api-reference/directives/use-cache-remote

✍️ 使い方
商品価格のようにリクエスト時に取得するが、全ユーザーで共有できるデータに適しています。

async function getProductPrice(productId: string) {
  'use cache: remote'

  const price = await db.products.getPrice(productId)
  return price
}

use cache: private

ユーザーごとに異なるデータをキャッシュする際に使用します。 cookies()headers()にアクセスでき、ユーザーが特定のリンクにホバーした時点で事前に取得(ランタイムプリフェッチ)することができます。

https://nextjs.org/docs/app/api-reference/directives/use-cache-private

✍️ 使い方
おすすめ商品やカート情報など、ログイン情報に基づいて内容が変わるデータに適しています。

async function getRecommendations(productId: string) {
  'use cache: private'

  const sessionId = (await cookies()).get('session-id')?.value || 'guest'
  const recommendations = await getPersonalizedRecommendations(productId, sessionId)
  return recommendations
}

キャッシュディレクティブの比較表

機能 use cache use cache: remote use cache: private
用途 静的な共有コンテンツ 動的コンテキストでの共有データ ユーザー固有のコンテンツ
実行タイミング ビルド時 リクエスト時 リクエスト時
キャッシュ保存先 サーバーサイド サーバーサイド クライアントサイドのみ
共有範囲 全ユーザー共通 全ユーザー共通 ユーザーごとに個別
cookies()使用 × × ⚪︎
headers()使用 × × ⚪︎
searchParams使用 × × ⚪︎
ランタイムプリフェッチ × × ⚪︎
ランタイムプリフェッチとは

ユーザーがリンクにホバーまたは表示した時点で、そのユーザーの現在のcookiesやheadersを使ってデータを事前取得する機能のことです。
https://nextjs.org/docs/app/guides/prefetching

⏳ キャッシュの有効期限設定

キャッシュの有効期限や更新タイミングを細かく制御できる関数が用意されています。

cacheLife

キャッシュの有効期限を設定するための新しい関数です。 事前定義されたプロファイル(seconds, minutes, hours, days, weeks, max)を使用するか、カスタムの時間設定が可能です。

https://nextjs.org/docs/app/api-reference/functions/cacheLife

3つのパラメータ
cacheLifeは、3つのパラメータでキャッシュの動作を制御します。

パラメータ 説明 動作
stale クライアント側でサーバーに確認せずキャッシュを使用する期間 この期間中はキャッシュをそのまま表示し、サーバーへの問い合わせは行わない
revalidate サーバー側でキャッシュを更新する頻度 この期間後、古いキャッシュを表示しながらバックグラウンドで更新
expire キャッシュが古いまま残れる最大期間 この期間を過ぎると動的フェッチに切り替わる(revalidateより長く設定)

✍️ 使い方

プロファイルを使用する場合
事前定義されたプロファイルを使うと、簡単にキャッシュ期間を設定できます。

import { cacheLife } from 'next/cache'

export async function getBlogPosts() {
  'use cache'
  cacheLife('hours')
  
  const posts = await fetch('https://blog.example.com/api/posts')
  return posts
}

カスタム設定を使用する場合
秒単位で細かく設定したい場合は、カスタム設定を使用します。

import { cacheLife } from 'next/cache'

export async function getBlogPosts() {
  'use cache'
  cacheLife({
    stale: 3600,      // 1時間
    revalidate: 900,  // 15分
    expire: 86400,    // 1日
  })
  
  const posts = await fetch('https://blog.example.com/api/posts')
  return posts
}

cacheTag

キャッシュされたデータにタグを付けて、後からrevalidateTagupdateTagで無効化できるようにする関数です。

https://nextjs.org/docs/app/api-reference/functions/cacheTag

✍️ 使い方
タグを付けることで、特定のキャッシュだけを狙って無効化できます。

import { cacheTag } from 'next/cache'

export async function getBlogPosts() {
  'use cache'
  cacheTag('posts')
  
  const posts = await fetch('https://blog.example.com/api/posts')
  return posts
}

unstable_cache

データベースクエリなど高コストな処理の結果をキャッシュする実験的な関数です。

https://nextjs.org/docs/app/api-reference/functions/unstable_cache

✍️ 使い方
データベースクエリの結果をキャッシュすることで、負荷を軽減できます。

import { unstable_cache } from 'next/cache'

const getCachedUser = unstable_cache(
  async () => {
    return { id: userId }
  },
  [userId], // キャッシュキー
  {
    tags: ['users'], // キャッシュタグ
    revalidate: 60,  // 再検証時間
  }
)

🧭 キャッシュの更新方法

データ更新後にキャッシュを無効化するための関数が用意されています。

updateTag

Next.js 16で新規追加された関数で、同じリクエスト内で即座にキャッシュを更新します。revalidateTagとは異なり、古いキャッシュを返さずに即座に新しいデータを取得します。

https://nextjs.org/docs/app/api-reference/functions/updateTag

✍️ 使い方
同じリクエスト内で確実に最新データを反映したい場合に使用します。

'use server'

import { updateTag } from 'next/cache'

export async function updateCart(itemId: string) {
  // データを更新
  await db.cart.add({ itemId })
  
  // キャッシュを即座に更新
  updateTag('cart')
}

revalidateTag、revalidatePath

既存の機能ですが、Cache Componentsでも引き続き使用できます。これらは段階的な更新(stale-while-revalidate) を行い、古いキャッシュを返しつつバックグラウンドで再検証します。

https://nextjs.org/docs/app/api-reference/functions/revalidateTag
https://nextjs.org/docs/app/api-reference/functions/revalidatePath

✍️ 使い方

revalidateTag
タグを指定してキャッシュを無効化します。

'use server'

import { revalidateTag } from 'next/cache'

export async function updateUser(id: string, data: UserData) {
  // データを更新
  await db.users.update({ id, data })
  
  // キャッシュを無効化
  revalidateTag('user', 'max')
}

revalidatePath
特定のページパスのキャッシュを無効化します。

'use server'

import { revalidatePath } from 'next/cache'

export async function createPost(post: Post) {
  // データを更新
  await db.posts.create(post)
  
  // ブログページのキャッシュを無効化
  revalidatePath('/blog')
}

🧩 ナビゲーション

キャッシュコンポーネントを有効にすると、React 19.2から利用可能な<Activity>コンポーネントを使用して、クライアントサイドナビゲーション中のコンポーネントの状態を保持できるようになります。
https://ja.react.dev/reference/react/Activity

従来は、別のページに移動すると前のページのコンポーネントが破棄されていましたが、<Activity>を使うとページを離れても状態を保持したまま非表示にするだけになります。

これにより:

  • ルート間を移動してもコンポーネントの状態が保持される
  • 前のページに戻ると状態がそのまま残っているため即座に復元される
  • 非表示中のエフェクトはクリーンアップされ、再表示時に再作成される

フォームの入力内容やUI要素の開閉状態などが維持されるため、ページ間の移動がよりスムーズになります。

動作イメージはこちらが参考になります👇
https://x.com/asidorenko_/status/1987916124453806316?s=20

✍️ 使い方
modeプロパティにvisibleまたはhiddenを指定して表示状態を切り替えます。タブを切り替えても各ページの状態(フォームの入力内容など)が保持されます。

import { Activity, useState } from 'react';

function TabView() {
  const [activeTab, setActiveTab] = useState('home');

  return (
    <>
      <nav>
        <button onClick={() => setActiveTab('home')}>ホーム</button>
        <button onClick={() => setActiveTab('contact')}>お問い合わせ</button>
      </nav>
      <hr />
      
      <Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}>
        <HomePage />
      </Activity>

      <Activity mode={activeTab === 'contact' ? 'visible' : 'hidden'}>
        <ContactPage />
      </Activity>
    </>
  );
}

📌 まとめ

キャッシュコンポーネントの登場で、キャッシュ管理を細かい粒度で制御できるようになり、パフォーマンスの向上や不要なフェッチリクエストをより柔軟に制御できるようになりました。 反面、実装の複雑さやキャッチアップコストのハードルも上がったと思います。

既存プロジェクトからの移行はかなりハードルが高い印象で、現在運用しているプロジェクトでも移行を視野に検証しましたが、工数がかかると判断してペンディングにしました。

ただ、新規プロジェクトなら設計段階で考慮しておけば積極的に採用してもよさそうです。 キャッシュコンポーネントは今後も改善されどんどん主流になっていくと思うので、採用を検討する価値はあると個人的に思っています。

📗 番外編: Turbopackもかなり熱い

Next.js 16からデフォルトのバンドラーがWebpackからTurbopackに変わりました。 これに伴い、開発体験(起動時間やページ遷移など)やビルド時間が高速化し、パフォーマンスが劇的に向上しました。

何が変わり、どのように変わったのかなどについては、以下の記事でまとめましたので、気になる方はこちらもご確認いただけると嬉しいです👇
https://zenn.dev/m_noto/articles/59223a9d2019af

👀 おわり

最後まで読んでくださり、ありがとうございました!☺️
この記事を通して、少しでも開発のお役に立てば幸いです!

個人ブログでも「技術選定に関すること」や「最新技術の分析・深掘り」など学びや知見を発信しています。もしご興味のある方はこちらからご確認いただけますと幸いです!
https://techbuild.app/blog

過去の執筆記事
https://zenn.dev/m_noto/articles/5e4c9f705f500b

Discussion