コンテンツにスキップ
App Routerはじめにキャッシュコンポーネント

キャッシュコンポーネント

キャッシュコンポーネントは、Next.jsにおけるレンダリングとキャッシングの新しいアプローチであり、何がいつキャッシュされるかを細かく制御しながら、部分的プレレンダリング(PPR)を通じて優れたユーザーエクスペリエンスを保証します。

キャッシュコンポーネント

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

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

キャッシュコンポーネントが有効になっている場合、Next.jsはデフォルトですべてのルートを動的として扱います。各リクエストは最新の利用可能なデータでレンダリングされます。ただし、ほとんどのページは静的および動的な部分で構成されており、すべての動的データがリクエストごとにソースから解決される必要はありません。

キャッシュコンポーネントを使用すると、データやUIの一部をキャッシュ可能としてマークできます。これにより、ページの静的な部分とともにプレレンダリングパスに含まれます。

キャッシュコンポーネントの前、Next.jsはページ全体を自動的に静的に最適化しようとしていましたが、動的なコードを追加した際に予期しない動作を引き起こす可能性がありました。

キャッシュコンポーネントは部分的プレレンダリング(PPR)と`use cache` を実装し、両方の長所を提供します。

Partially re-rendered Product Page showing static nav and product information, and dynamic cart and recommended products

ユーザーがルートにアクセスすると

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

🎥 ウォッチ: PPRとは何か、そしてその仕組み → YouTube (10分)

仕組み

キャッシュコンポーネントは、レンダリングを制御するための3つの主要なツールを提供します

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

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

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

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

fetch呼び出しやデータベースクエリ(`db.query(...)`)のような動的データは、リクエスト間で変更される可能性がありますが、ユーザー固有のものではありません。`connection` APIはメタ動的です — 返される実際のデータはありませんが、ユーザーのナビゲーションを待機していることを表します。これらのAPIを使用するコンポーネントをSuspense境界でラップして、ストリーミングを有効にしてください。

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

  • fetchリクエスト
  • データベースクエリ
  • connection

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を含む)は静的なシェルとしてプレレンダリングされますが、境界内のコンテンツは準備ができ次第ストリーミングされます。

キャッシュコンポーネントでSuspenseを使用する方法を以下に示します

app/page.tsx
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は静的コンテンツとfallback UIをプレレンダリングします。動的コンテンツは、ユーザーがルートをリクエストするまで延期されます。

知っておくと良いこと: コンポーネントをSuspenseでラップしても、それが動的になるわけではありません。動的になるのはAPIの使用方法です。Suspenseは、動的コンテンツをカプセル化し、ストリーミングを可能にする境界として機能します。

Suspense境界の欠落

キャッシュコンポーネントは、動的コードがSuspense境界でラップされていることを強制します。忘れると、Uncached data was accessed outside of <Suspense>エラーが発生します。

Uncached data was accessed outside of <Suspense>

これにより、ページ全体のレンダリングが遅延し、ユーザーエクスペリエンスが悪化します。Next.jsはこのエラーを使用して、アプリがすべてのナビゲーションで即座にロードされることを保証します。

これを修正するには、次のいずれかを行います。

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

または

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

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

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

ストリーミングの仕組み

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

Diagram showing partially rendered page on the client, with loading UI for chunks that are being streamed.

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

Diagram showing parallelization of route segments during streaming, showing data fetching,rendering, and hydration of individual chunks.

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

`use cache` の使用

Suspense境界が動的コンテンツを管理するのに対し、`use cache`ディレクティブは、あまり変更されないデータまたは計算をキャッシュするために使用できます。

基本的な使用法

`use cache` をページ、コンポーネント、または非同期関数にキャッシュし、`cacheLife` で有効期限を定義します。

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

注意点

`use cache` を使用する際は、これらの制約に注意してください。

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

Server Actionsと同様に、キャッシュされた関数の引数はシリアライズ可能である必要があります。これは、プリミティブ、プレーンオブジェクト、および配列を渡すことはできますが、クラスインスタンス、関数、またはその他の複雑な型は渡せないことを意味します。

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

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

app/cached-wrapper.tsx
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でタグ付けし、Server ActionsのupdateTagまたはrevalidateTagを使用してミューテーション後に再検証します。これにより、即時更新が可能になります。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を使用

Stale-while-revalidate動作で適切にタグ付けされたキャッシュエントリのみを無効にしたい場合は、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リファレンスを参照してください。

キャッシュコンポーネントの有効化

cacheComponentsオプションをNext configファイルに追加することで、キャッシュコンポーネント(PPRを含む)を有効にできます。

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

ルートセグメント構成への影響

キャッシュコンポーネントが有効になると、いくつかのルートセグメント構成オプションは不要になるか、サポートされなくなります。変更点と移行方法を以下に示します。

dynamic = "force-dynamic"

不要です。キャッシュコンポーネントが有効になっている場合、すべてのページはデフォルトで動的になるため、この構成は不要です。

// 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`に置き換えてください。関連するルートの各LayoutおよびPageに`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>
}

キャッシュコンポーネントの前と後

キャッシュコンポーネントがメンタルモデルをどのように変更するかを理解する

キャッシュコンポーネントの前

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

キャッシュコンポーネントを使用

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

動的API

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

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

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

これで、Userコンポーネントの周りにSuspense境界ができたため、PageをSkeleton UIでプレレンダリングし、特定のユーザーがリクエストを行ったときに<User /> UIをストリーミングできます。

app/page.tsx
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
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
export async function Table({ sortPromise }: { sortPromise: Promise<string> }) {
  const sort = (await sortPromise) === 'true'
  return '...'
}

よくある質問

これは部分的プレレンダリング(PPR)に取って代わるものですか?

いいえ。キャッシュコンポーネントはPPRを機能として実装します。古い実験的なPPRフラグは削除されましたが、PPRは引き続き提供されます。

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

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

キャッシュすべきものは、UIのローディング状態をどのようにしたいかの関数です。データがランタイムデータに依存せず、一定期間複数のリクエストに対してキャッシュされた値が提供されても問題ない場合は、`use cache`と`cacheLife`を使用してその動作を記述してください。

更新メカニズムを持つコンテンツ管理システムの場合、より長いキャッシュ期間を持つタグの使用を検討し、静的な初期UIを再検証可能としてマークするために`revalidateTag`に依存してください。このパターンにより、コンテンツが実際に変更されたときに更新しながら、高速でキャッシュされたレスポンスを提供できます。キャッシュを事前に期限切れにするのではなく。

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

キャッシュされたデータを`cacheTag`でタグ付けし、その後、updateTagまたはrevalidateTagをトリガーします。