Menu

Cache Components

Cache Componentsは、Next.jsにおけるレンダリングとキャッシングの新しいアプローチです。何がキャッシュされるか、いつキャッシュされるかに対してきめ細かい制御を提供しながら、**Partial Prerendering(PPR)**を通じて優れたユーザー体験を確保します。

Cache Components

動的アプリケーションを開発する際、2つの主要なアプローチのバランスを取る必要があります。

  • 完全に静的なページは高速に読み込まれますが、パーソナライズされたデータやリアルタイムデータを表示できません
  • 完全に動的なページは新しいデータを表示できますが、各リクエストですべてをレンダリングする必要があり、初期読み込みが遅くなります

Cache Componentsを有効にすると、Next.jsはすべてのルートをデフォルトで動的として扱います。すべてのリクエストは利用可能な最新データでレンダリングされます。ただし、ほとんどのページは静的な部分と動的な部分の両方で構成されており、すべての動的データをすべてのリクエストでソースから解決する必要はありません。

Cache Componentsを使うと、データやUIの一部をキャッシュ可能としてマークでき、ページの静的な部分と一緒にプリレンダリングパスに含めることができます。

Cache Components以前、Next.jsはページ全体を自動的に静的に最適化しようとしていたため、動的コードを追加する際に予期しない動作につながる可能性がありました。

Cache Componentsは**Partial Prerendering(PPR)**とuse cacheを実装し、両方の世界の利点をもたらします。

静的なナビゲーションと製品情報、および動的なカートと推奨製品を表示するPartially re-renderedされた商品ページ

ユーザーがルートにアクセスする場合:

  • サーバーはキャッシュされたコンテンツを含む静的シェルを送信し、高速な初期読み込みを確保します
  • Suspense境界でラップされた動的セクションはシェル内にフォールバックUIを表示します
  • 動的な部分のみがレンダリングされてフォールバックを置き換え、準備ができ次第並行してストリーミングされます
  • use cacheを使ってキャッシュすることで、初期シェルに動的データを含めることができます

**🎥 動画:**PPRとその仕組み → YouTube(10分)

仕組み

補足: Cache Componentsはオプトイン機能です。Next.js設定ファイルでcacheComponentsフラグをtrueに設定して有効にします。詳しくはCache Componentsの有効化をご覧ください。

Cache Componentsはレンダリングを制御するための3つの主要なツールを提供します。

1. ランタイムデータのための Suspense

実際のユーザーがリクエストを行う際、ランタイムにのみ利用可能なデータがあります。cookiesheaderssearchParamsなどのAPIはリクエスト固有の情報にアクセスします。これらのAPIを使用するコンポーネントをSuspense境界でラップすると、ページの残りの部分を静的シェルとしてプリレンダリングできます。

ランタイムAPIには以下が含まれます:

2. 動的データのための Suspense

fetch呼び出しやデータベースクエリ(db.query(...))のような動的データはリクエスト間で変わりますが、ユーザー固有のものではありません。connectionAPIはメタ動的です。戻すべき実際のデータはありませんが、ユーザーナビゲーションを待つことを表します。これらを使用するコンポーネントをSuspense境界でラップすると、ストリーミングが有効になります。

動的データパターンには以下が含まれます:

3. use cacheを使ったキャッシュされたデータ

サーバーコンポーネントにuse cacheを追加すると、キャッシュされ、プリレンダリングシェルに含まれます。キャッシュされたコンポーネント内からはランタイムAPIを使用できません。ユーティリティ関数をuse cacheとしてマークして、サーバーコンポーネントから呼び出すこともできます。

export async function getProducts() {
  'use cache'
  const data = await db.query('SELECT * FROM products')
  return data
}

Suspense境界の使用

React Suspense境界を使うと、動的またはランタイムデータをラップするときに使用するフォールバックUIを定義できます。

境界の外のコンテンツ(フォールバックUIを含む)は静的シェルとしてプリレンダリングされ、境界内のコンテンツは準備ができたらストリーミングされます。

Cache ComponentsでSuspenseを使う方法は以下の通りです。

app/page.tsx
TypeScript
import { Suspense } from 'react'
 
export default function Page() {
  return (
    <>
      <h1>This will be pre-rendered</h1>
      <Suspense fallback={<Skeleton />}>
        <DynamicContent />
      </Suspense>
    </>
  )
}
 
async function DynamicContent() {
  const res = await fetch('http://api.cms.com/posts')
  const { posts } = await res.json()
  return <div>{/* ... */}</div>
}

ビルド時に、Next.jsは静的コンテンツとfallbackUIをプリレンダリングし、動的コンテンツはユーザーがルートをリクエストするまで遅延させます。

補足: Suspenseにコンポーネントをラップすることが動的にするわけではありません。APIの使用が動的にします。Suspenseは動的コンテンツをカプセル化し、ストリーミングを有効にする境界として機能します。

Suspense境界の欠落

Cache Componentsは、動的コードがSuspense境界にラップされることを強制します。忘れた場合、<Suspense>外でキャッシュされていないデータにアクセスされましたエラーが表示されます:

<Suspense>外でキャッシュされていないデータにアクセスされました

これにより、ページ全体のレンダリングが遅延し、ユーザー体験が低下します。Next.jsはこのエラーを使用して、すべてのナビゲーションでアプリが瞬時に読み込まれるようにします。

これを修正するには、以下のいずれかを実行できます。

コンポーネントを<Suspense>境界でラップします。 これにより、Next.jsはコンテンツが準備できたらユーザーにストリーミングでき、アプリの残りをブロックせずに済みます。

または

非同期awaitをCache Component("use cache")に移動します。 これにより、Next.jsはHTMLドキュメントの一部としてコンポーネントを静的にプリレンダリングでき、ユーザーに瞬時に表示されます。

パラメータ、クッキー、ヘッダーなどのリクエスト固有の情報は、静的プリレンダリング中は利用できないため、<Suspense>でラップする必要があります。

このエラーは、静的シェルを瞬時に取得する代わりに、ユーザーが表示するものなしでブロッキングランタイムレンダリングに当たる状況を防ぐのに役立ちます。これを修正するには、Suspense境界を追加するか、use cacheを使ってその代わりに作業をキャッシュします。

ストリーミングの仕組み

ストリーミングはルートをチャンクに分割し、準備ができたらクライアントに段階的にストリーミングします。 これにより、ユーザーはコンテンツ全体のレンダリングが完了する前に、ページの一部を即座に確認できます。

ブラウザ上でレンダリングされているページを部分的に表示する図。ストリーミング中のチャンクには読み込みUIが表示されます。

部分的なプリレンダリングにより、初期UIをブラウザにすぐに送信でき、動的な部分はレンダリングされます。これにより、UIまでの時間が短縮され、プリレンダリングされたUIの量に応じて合計リクエスト時間も短縮される可能性があります。

ストリーミング中にルートセグメントの並列化を示す図。個々のチャンクのデータ取得、レンダリング、ハイドレーションを表示します。

ネットワークオーバーヘッドを削減するため、静的HTMLとストリーミングされた動的な部分を含む完全なレスポンスが単一のHTTPリクエストで送信されます。これにより追加のラウンドトリップが回避され、初期読み込みと全体的なパフォーマンスが向上します。

use cacheの使用

Suspense境界が動的コンテンツを管理する一方で、use cacheディレクティブはあまり変わらないデータや計算のキャッシングに利用できます。

基本的な使い方

ページ、コンポーネント、または非同期関数にuse cacheを追加してキャッシュし、cacheLifeでライフタイムを定義します。

app/page.tsx
TypeScript
import { cacheLife } from 'next/cache'
 
export default async function Page() {
  'use cache'
  cacheLife('hours')
  // fetch or compute
  return <div>...</div>
}

注意点

use cacheを使用する場合、これらの制約に注意してください:

引数はシリアライズ可能である必要があります

サーバーアクションと同様に、キャッシュされた関数への引数はシリアライズ可能である必要があります。つまり、プリミティブ、プレーンオブジェクト、配列は渡せますが、クラスインスタンス、関数、その他の複雑な型は渡せません。

イントロスペクションなしでシリアライズ不可能な値を受け入れる

シリアライズ不可能な値をイントロスペクションしない限り、引数として受け入れることができます。ただし、それらを返すことはできます。これにより、サーバーまたはクライアントコンポーネントを子として受け入れるキャッシュされたコンポーネントのようなパターンが可能になります。

app/cached-wrapper.tsx
TypeScript
import { ReactNode } from 'react'
 
export async function CachedWrapper({ children }: { children: ReactNode }) {
  'use cache'
  // Don't introspect children, just pass it through
  return (
    <div className="wrapper">
      <header>Cached Header</header>
      {children}
    </div>
  )
}

動的な入力を渡すのを避ける

イントロスペクションを避けない限り、use cache関数に動的またはランタイムデータを渡さないようにする必要があります。cookies()headers()、またはその他のランタイムAPIからの値を引数として渡すと、プリレンダリング時にキャッシュキーを決定できないため、エラーが発生します。

タギングと再検証

cacheTagでキャッシュされたデータにタグを付け、updateTagをサーバーアクションで使用して即座に更新し、またはrevalidateTagで遅延更新を使用してミューテーション後に再検証します。

updateTagの場合

同じリクエスト内でキャッシュされたデータを即座に失効させて更新する必要があるときにupdateTagを使用します:

app/actions.ts
import { cacheTag, updateTag } from 'next/cache'
 
export async function getCart() {
  'use cache'
  cacheTag('cart')
  // fetch data
}
 
export async function updateCart(itemId: string) {
  'use server'
  // write data using the itemId
  // update the user cart
  updateTag('cart')
}

revalidateTagの場合

適切にタグ付けされたキャッシュエントリのみを無効にしたい場合にrevalidateTagを使用し、ステール・ホワイル・リバリデート動作があります。これは最終的一貫性を許容できる静的コンテンツに最適です。

app/actions.ts
import { cacheTag, revalidateTag } from 'next/cache'
 
export async function getPosts() {
  'use cache'
  cacheTag('posts')
  // fetch data
}
 
export async function createPost(post: FormData) {
  'use server'
  // write data using the FormData
  revalidateTag('posts', 'max')
}

詳細な説明と使用例については、use cache APIリファレンスを参照してください。

Cache Componentsの有効化

Next.js設定ファイルにcacheComponentsオプションを追加して、Cache Components(PPRを含む)を有効にできます:

next.config.ts
TypeScript
import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  cacheComponents: true,
}
 
export default nextConfig

ルートセグメント設定への影響

Cache Componentsが有効な場合、複数のルートセグメント設定オプションは不要または非対応になります。ここで変更点と移行方法を示します:

dynamic = "force-dynamic"

不要です。 Cache Componentsが有効な場合、すべてのページがデフォルトで動的であるため、この設定は不要です。

// Before - No longer needed
export const dynamic = 'force-dynamic'
 
export default function Page() {
  return <div>...</div>
}
// After - Just remove it, pages are dynamic by default
export default function Page() {
  return <div>...</div>
}

dynamic = "force-static"

use cacheに置き換えます。 関連するルートに対して、各レイアウトとページにuse cacheを追加する必要があります。

注:force-static以前はcookies()のようなランタイムAPIの使用を許可していましたが、これはもはやサポートされていません。use cacheを追加してランタイムデータに関するエラーが表示される場合、ランタイムAPIの使用を削除する必要があります。

// Before
export const dynamic = 'force-static'
 
export default async function Page() {
  const data = await fetch('https://api.example.com/data')
  return <div>...</div>
}
// After - Use 'use cache' instead
export default async function Page() {
  'use cache'
  const data = await fetch('https://api.example.com/data')
  return <div>...</div>
}

revalidate

cacheLifeに置き換えます。 cacheLife関数を使用してキャッシュ期間を定義します。ルートセグメント設定の代わりにこれを使用してください。

// Before
export const revalidate = 3600 // 1 hour
 
export default async function Page() {
  return <div>...</div>
}
// After - Use cacheLife
import { cacheLife } from 'next/cache'
 
export default async function Page() {
  'use cache'
  cacheLife('hours')
  return <div>...</div>
}

fetchCache

不要です。 use cacheにより、キャッシュされたスコープ内のすべてのデータ取得は自動的にキャッシュされるため、fetchCacheは不要です。

// Before
export const fetchCache = 'force-cache'
// After - Use 'use cache' to control caching behavior
export default async function Page() {
  'use cache'
  // All fetches here are cached
  return <div>...</div>
}

runtime = 'edge'

非対応です。 Cache ComponentsはNode.jsランタイムが必要であり、Edge Runtimeでは エラーをスローします。

Cache Components前後での比較

Cache Componentsがメンタルモデルをどのように変更するかを理解しましょう:

Cache Components以前

  • デフォルトで静的:Next.jsはオプトアウトしない限り、できるだけ多くのことをプリレンダリングしてキャッシュしようとしていました
  • ルートレベルの制御dynamicrevalidatefetchCacheなどのスイッチがページ全体のキャッシング制御を行いました
  • fetchの制限fetch単独の使用は不完全でした。直接データベースクライアントや他のサーバー側IOはカバーされず、ネストされたfetchが動的に切り替わる場合(例:{ cache: 'no-store' })、ルート動作全体が意図しないで変わる可能性がありました

Cache Components利用時

  • デフォルトで動的:すべてがデフォルトで動的です。use cacheを追加して、役立つ部分をキャッシュすることを決めます
  • きめ細かい制御:ファイル/コンポーネント/関数レベルのuse cachecacheLifeが必要な場所でキャッシング制御を行います
  • ストリーミングは残ります<Suspense>またはloading.(js|tsx)ファイルを使用して、シェルが即座に表示される動的な部分をストリーミングします
  • fetchを超えてuse cacheディレクティブの使用により、すべてのサーバーIO(データベース呼び出し、API、計算)にキャッシングを適用でき、fetchだけではありません。ネストされたfetch呼び出しは、動作が明示的なキャッシュ境界とSuspenseで支配されるため、ルート全体を黙って切り替えることはありません

動的API

cookies()のようなランタイムAPIにアクセスする場合、Next.jsはこのコンポーネントの上のフォールバックUIのみをプリレンダリングします。

この例では、フォールバックが定義されていないため、Next.jsは1つを提供するようにと指示するエラーを表示します。<User />コンポーネントはcookiesAPIを使用するため、Suspenseでラップされる必要があります:

app/user.tsx
TypeScript
import { cookies } from 'next/headers'
 
export async function User() {
  const session = (await cookies()).get('session')?.value
  return '...'
}

ここまで、UserコンポーネントがSuspense境界でラップされています。特定のユーザーがリクエストを行うと、SkeletonUIでページをプリレンダリングして、<User />UIをストリーミングできます

app/page.tsx
TypeScript
import { Suspense } from 'react'
import { User, AvatarSkeleton } from './user'
 
export default function Page() {
  return (
    <section>
      <h1>This will be pre-rendered</h1>
      <Suspense fallback={<AvatarSkeleton />}>
        <User />
      </Suspense>
    </section>
  )
}

動的プロップの渡し方

コンポーネントはその値がアクセスされる場合のみ、動的レンダリングにオプトインします。たとえば、<Page />コンポーネントからsearchParamsを読む場合、この値を別のコンポーネントにプロップとして転送できます:

app/page.tsx
TypeScript
import { Table, TableSkeleton } from './table'
import { Suspense } from 'react'
 
export default function Page({
  searchParams,
}: {
  searchParams: Promise<{ sort: string }>
}) {
  return (
    <section>
      <h1>This will be pre-rendered</h1>
      <Suspense fallback={<TableSkeleton />}>
        <Table searchParams={searchParams.then((search) => search.sort)} />
      </Suspense>
    </section>
  )
}

テーブルコンポーネント内で、searchParamsから値にアクセスするとコンポーネントが動的になり、ページの残りはプリレンダリングされます。

app/table.tsx
TypeScript
export async function Table({ sortPromise }: { sortPromise: Promise<string> }) {
  const sort = (await sortPromise) === 'true'
  return '...'
}

よくある質問

これはPartial Prerendering(PPR)に置き換わりますか?

いいえ。Cache ComponentsはPPRを機能として実装します。古い実験的なPPRフラグは削除されていますが、PPRは存続しています。

PPRは静的シェルとストリーミングインフラストラクチャを提供し、use cacheにより、有益な場合にその最適化された動的出力をそのシェルに含めることができます。

最初に何をキャッシュすべきですか?

キャッシュすべき内容はUIの読み込み状態がどうあるべきかの関数である必要があります。データがランタイムデータに依存せず、キャッシュされた値が期間中に複数のリクエストに対して処理されることに問題がない場合、use cachecacheLifeとともに使用してその動作を説明します。

更新メカニズムを持つコンテンツ管理システムの場合、より長いキャッシュ期間でタグを使用し、revalidateTagに依存して静的初期UIを再検証の準備ができたものとしてマークすることを検討してください。このパターンにより、キャッシュをプリエンプティブに失効させるのではなく、実際に変わったときにコンテンツを更新して、高速でキャッシュされたレスポンスを処理しながら提供できます。

キャッシュされたコンテンツを高速に更新するにはどうすればいいですか?

cacheTagを使用してキャッシュされたデータにタグを付け、updateTagまたはrevalidateTagをトリガーします。