Cache ComponentsにおけるSuspenseの役割について
この記事は レバテック開発部 Advent Calendar 2025 11日目の記事です。
はじめに
先日、Next.js 16がリリースされました。様々な改善が見られる中、特に注目されているのはやはりCache Componentsでしょう。
本記事では、Cache Components有効化にともなってSuspenseの役割がどう変わるのか、どのようにSuspenseを配置すべきかについてまとめていこうと思います。
Cache Componentsって何?
まずはCache Componentsの概要と主な機能について簡単に説明します。
Cache Componentsは、Next.js 16で追加された機能で、use cacheによるキャッシュ制御と、ページ内での静的・動的・キャッシュ済みコンテンツの混在を可能にします。
Cache Componentsを使用するには、next.config.ts(またはnext.config.js)でcacheComponentsフラグをtrueに設定する必要があります。
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;
部分的な静的・動的レンダリング
従来のNext.jsでは、レンダリング手法をページ単位でしか定められず、ページ内に一つでも動的コンテンツやDynamic APIを使用しているコンポーネントがあればページ全体がSSRされ、その分初期描画が遅いという課題がありました。
しかしCache Componentsにより、ページ内の部分ごとに静的か動的かを分けられるようになりました。
// ↓ 静的(ビルド時に生成、高速)
<Header />
// ↓ キャッシュ(初回だけ取得、再利用)
<CachedContent />
// ↓ 動的(毎回最新データを取得)
<Suspense fallback={<Loading />}>
<UserProfile />
</Suspense>
これをPartial Prerendering(PPR)と呼び、部分的にプリレンダリング、つまりビルド時に静的HTMLを生成し、最新の情報が必要な部分についてはリクエスト時に動的にレンダリングすることを可能にします。
use cacheによる複雑なキャッシュAPIからの脱却
Next.js 15では、データソースごとに異なるキャッシング手法(fetchのオプション、unstable_cache API、ルートセグメント設定)を使い分ける必要がありました。
しかしCache Componentsでは、use cacheディレクティブでこれらを全て扱えるようになります。
import { cacheLife, cacheTag } from "next/cache";
async function getProducts() {
"use cache";
cacheLife("hours");
cacheTag("products");
const data = await fetch("https://api.example.com/products");
return data;
}
また、use cacheは関数、コンポーネント、ページレベルで柔軟に適用できます。
これにより、複雑とされていたキャッシュ周りがある程度直感的に扱えるようになりました。
Suspenseの新しい役割:キャッシュとの境界線
Suspenseの役割の話をする上で重要な、Cache Componentsを有効化した際によく発生するエラーの説明をします。
例えば以下のようなコードは、Cache Componentsが有効な場合にエラーが発生します。
export default function Page() {
return (
<>
<DynamicCotent />
</>
);
}
export const DynamicCotent = async() => {
const res = await fetch(~~);
const post = await res.json();
return (
<div>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
);
}
エラー文
Error: Uncached data was accessed outside of `<Suspense>`
直訳すると、「<Suspense> の外側でキャッシュされていないデータにアクセスしました」と怒られています。なにやらSuspenseで囲う必要がありそうなエラー文ですね。
エラー要因
Cache Componentsが有効な環境では、Next.jsはビルド時に可能な限りコンテンツを静的シェルとして生成しようとします。しかし、use cacheでキャッシュされていない非同期データは、毎回のリクエストで取得される動的なデータとして扱われます。
動的なデータにアクセスする部分は、ビルド時には実行できないため、静的シェルには含められません。そのため、これらの部分はSuspense境界の内側に配置して、「この部分はリクエスト時に動的にレンダリングする」ことを明示的に宣言する必要があります。
<Suspense fallback={<div>Loading...</div>}>
{/* Suspenseの内側は動的コンテンツを配置可能 */}
<DynamicContent />
</Suspense>
境界線としてのSuspense
このエラー内容と解消方法から読み取れることとして、従来のSuspenseの主な役割であるローディング状態の宣言的管理に加え、「キャッシュ可能な静的コンテンツと、リクエスト時に生成される動的コンテンツの境界線」 としての役割を持つようになったと言えるでしょう。
この変化により、「どこにSuspenseを置くか」という設計判断が、より一層アプリケーションのパフォーマンスとユーザー体験に影響するようになります。
use cacheとSuspenseの使い分け
「Suspenseをどこに置くか」以前に、まずどんな時にuse cacheを使い、どんな時にSuspenseを使えば良いかについて、基本的な判断基準を紹介します。
1. Dynamic APIを使用する場合
→ Suspenseで囲む。これらのAPIはuse cacheスコープ内では使用できません(リクエストごとに変化する動的情報のため)。
2. 更新頻度が低いデータの場合
→ use cache + cacheLifeを使用。商品一覧やブログ記事など頻繁に変更されないデータ。静的シェルに含まれ、全ユーザー間でキャッシュ共有されます。
3. 常に最新のデータが必要な場合
→ Suspenseで囲む(use cacheは使わない)。通知数、在庫状況など、リクエストごとに変化するデータ。毎回サーバーで最新データを取得します。
実践例
- 静的部分(Header、Footer)→ そのまま表示
-
共有可能データ(記事、商品一覧)→
use cache+cacheLife - ユーザー固有データ(ブックマーク、通知数)→ Suspense
例えば、ブログ記事ページなら:
- 記事本文 →
use cacheで1時間(全ユーザー共有) - 関連記事 →
use cacheで24時間(全ユーザー共有) - ブックマーク状態 → Suspense(常に最新)
Suspense配置のポイント
次にSuspenseを使用する上での配置についてですが、どこに置くかでアプリケーションのパフォーマンスとユーザー体験が変わります。
できるだけ動的な部分に近く配置する
静的シェルを最大化するには、動的なコンテンツだけをSuspenseで囲むのが基本です。
仮に静的コンテンツを動的コンテンツと一緒にSuspenseで囲うと動的コンテンツが表示可能になるまで、静的コンテンツも表示されなくなり、無駄のある設計となってしまいます。
// 悪い例:ページ全体をSuspenseで囲む
<Suspense fallback={<Loading />}>
<StaticContent /> {/* 静的なのにSuspense内 */}
<CacheableContent /> {/* use cacheで静的シェルに含められる */}
<DynamicContent /> {/* 動的 */}
</Suspense>
// 良い例:動的な部分だけSuspense
<>
<StaticContent /> {/* 静的シェルに含まれる */}
<CacheableContent /> {/* use cacheで静的シェルに含まれる */}
<Suspense fallback={<Skeleton />}>
<DynamicContent /> {/* 動的 */}
</Suspense>
</>
まとめ
Next.js 16のCache Componentsにより、Suspenseは「ローディング状態の宣言的管理」という基本的な役割に加え、「キャッシュと動的コンテンツの境界線を定義する設計要素」 という役割を持つようになったという話でした。
本記事の内容を踏まえ、パフォーマンスが重要なプロダクトでは積極的にcacheComponentsフラグをtrueにしつつ、キャッシュの使い所とSuspenseの境界を意識しながらレンダリングを最適化していきいましょう。
Discussion