🕌

Next.jsの4つのキャッシュメカニズムについて

2024/03/04に公開

概要

Next.jsのキャッシュについてドキュメントの確認

https://nextjs.org/docs/app/building-your-application/caching

Next.jsはレンダリングワークとデータリクエストをキャッシュすることで、アプリケーションのパフォーマンスを改善し、コストを削減します。
このページでは、Next.jsのキャッシュメカニズム、それらを構成するために使用できるAPI、およびこれらがどのように相互作用するかについて詳しく説明します。

4つのキャッシュメカニズム

Mechanism What Where Purpose Duration
Request Memoization Return values of functions Server Re-use data in a React Component tree Per-request lifecycle
Data Cache Data Server Store data across user requests and deployments Persistent (can be revalidated)
Full Route Cache HTML and RSC payload Server Reduce rendering cost and improve performance Persistent (can be revalidated)
Router Cache RSC Payload Client Reduce server requests on navigation User session or time-based

デフォルトでは、Next.jsはパフォーマンスを向上させ、コストを削減するために可能な限りキャッシュします。これは、opt out(キャッシュ機能を除外、無効化)しない限り、ルートが静的にレンダリングされ、データリクエストがキャッシュされるということを意味します。下の図は、ルートがビルド時に静的にレンダリングされ、静的なルートが初めて訪れた場合のデフォルトのキャッシング動作を示しています。

キャッシュの動作は、ルートが静的にレンダリングされるか動的にレンダリングされるか、データがキャッシュされているかキャッシュされていないか、リクエストが初回の訪問か後続のナビゲーションかによって変わります。ユースケースに応じて、個々のルートやデータリクエストのキャッシング動作を構成できます。

Request Memoization

Reactはfetch APIを拡張して同じURLとオプションをもつリクエストを自動的にMemoization(メモ化)します。
これは、Reactコンポーネントツリー内の複数の場所で同じデータのfetch関数を呼び出すことができ、関数はただ一度だけ実行されるということを意味します。

例えば、同じデータをルート全体で使用する必要がある場合(例:Layout、Page、および複数のコンポーネントで)、データをツリーの上でフェッチしてからコンポーネント間でpropsを渡す必要はありません。propsを渡す代わりに、同じデータに対してネットワークを介して複数のリクエストを行う際のパフォーマンスの影響を心配せずに、それを必要とするコンポーネントでデータをfetchできます。

async function getItem() {
  // The `fetch` function is automatically memoized and the result
  // is cached
  const res = await fetch('https://.../item/1')
  return res.json()
}
 
// This function is called twice, but only executed the first time
const item = await getItem() // cache MISS
 
// The second call could be anywhere in your route
const item = await getItem() // cache HIT

これにより、複数コンポーネントで同一のデータを利用する場合も、上位コンポーネントからpropsでデータを渡す必要はありません。
代わりに、同じデータに対してネットワーク上で複数のリクエストを行うことによるパフォーマンスへの影響を心配することなく、必要なコンポーネントでデータをフェッチできます。

Request Memoizationの動作

  • ルートのレンダリング中に、初回リクエストが呼ばれた時は、そのリクエスト結果はメモリに存在しないため、キャッシュMISSとなる。
  • キャッシュMISSとなったので、その関数は実行される。そしてデータが外部ソースからfetchされて、結果はメモリに保存される。
  • 同じレンダーパス内での後続のリクエスト関数の呼び出しはキャッシュHITとなる。なので関数は実行されることなく、メモリからデータが返される。
  • ルートがレンダリングされて、レンダリングパスが完了すると、メモリはリセットされ、リクエストのmemoization entries(メモ化エントリ)はクリアされる。

Good to know:

  • Request memoizationはReact機能で, Next.jsの機能ではありません. 他のキャッシュメカニズムとどのように相互作用をするかを示すためにここに含まれています。
  • MemoizationはfetchリクエストのGET methodのみ適用されます。
  • MemoizationはReact Component treeのみ適用されます。つまり:
    • generateMetadata, generateStaticParams, Layouts, Pagesと他の Server Componentsのfetch requestsに適用されます。
    • Route Handlersのfetch requestsはReact component treeの一部ではないため適用されません。
  • fetchが適してない場合(例えばdatabase clientsCMS clientsGraphQL clients)、メモ化するためにReact cache functionを使うことができます。

Duration(存続期間)

キャッシュは、Reactコンポーネントツリーのレンダリングが完了するまで、サーバーリクエストの間、存続します。

Revalidating(再検証)

memoization(メモ化)はサーバーリクエスト間で共有されず、レンダリング間だけ適用されるため、revalidate(再検証)する必要はありません。

Opting out(除外、無効化)

fetchリクエストのmemoization(メモ化)を無効にするには, AbortController signalをリクエストに渡します。

const { signal } = new AbortController()
fetch(url, { signal })

Data Cache

Next.jsにはサーバーリクエストやデプロイメントをまたいでのデータfetchの永続化を行う、組み込みのData Cacheがあります。これが可能なのは、Next.jsがサーバー上の各リクエストを独自の永続的なキャッシュセマンティクスにセットできるようにしたためです。

Good to know:

ブラウザでは、fetchのcacheオプションはリクエストがブラウザのHTTPキャッシュとどのように相互作用するかを示しますが、Next.jsでは、cacheオプションはサーバーサイドのリクエストがサーバーのデータキャッシュとどのように相互作用するかを示します。

デフォルトでは、fetchを使うデータのリクエストはキャッシュされます
キャッシュの動作を構成するには、fetchcachenext.revalidateオプションを使うことができます。

Data Cacheの動作

  • レンダリング中に初回のfetchリクエストが呼ばれる、Next.jsはData Cacheにキャッシュされたレスポンスがあるかチェックする。
  • キャッシュされたレスポンスが見つかった場合、レスポンスはすぐにreturnされmemoized(メモ化)される。
  • キャッシュされたレスポンスが見つからなかった場合, リクエストがデータソースに対して行われ、結果がData Cacheに保存され、memoized(メモ化)される。
  • キャッシュしないデータでは (例: { cache: 'no-store' })、結果は常にデータソースからfetchされて、memoized(メモ化)される。
  • データがキャッシュされるかどうかにかかわらず、リクエストは常にmemoized(メモ化)され、Reactのレンダーパスの間に同じデータに重複したリクエストを行うことを防ぎます。

Data CacheとRequest Memoizationの違い

両方のキャッシュメカニズムはキャッシュデータを再利用することでパフォーマンスの向上に役立ちますが、Data Cacheはリクエストやデプロイメントをまたいで永続的です。一方memoization(メモ化)の期間はリクエストの生存期間のみです。

memoization(メモ化)では、レンダリングサーバーからデータキャッシュサーバー(CDN やエッジ ネットワークなど) またはデータソース(データベースやCMSなど)までネットワーク境界を越える必要がある、同じレンダーパスの中で重複したリクエストの数を減少させることができます。
Data Cacheでは、オリジンデータソースへのリクエストの数を減少させることができます。

Duration(存続期間)

Data Cacherevalidate(再検証)やopt-out(除外、無効化)をしない限り、リクエストやデプロイメントをまたいで永続化されます。

Revalidating(再検証)

キャッシュされたデータは次の2つの方法でrevalidated(再検証)することができます:

  • Time-based Revalidation(時間ベースの再検証): 一定期間が過ぎて、新しいリクエストが行われた時、データをrevalidate(再検証)します。 頻繁に変更されない、新鮮さが重要でないデータに対して有用です。
  • On-demand Revalidation(要求ベースの再検証): イベント (例: フォームの送信)に基づいてデータをrevalidate(再検証)します。On-demand revalidation(要求ベースの再検証)は一度に複数のグループのデータをrevalidate(再検証)するためにタグベースパスベースのアプローチができます。できるだけ早く最新のデータが表示されるようにしたい場合に有用です(例: ヘッドレスCMSからのコンテンツが更新された時)。

Time-based Revalidation(時間ベースの再検証)

一定の時間間隔でデータをrevalidate(再検証)するには、fetchnext.revalidateオプションを使うことができ、秒単位でキャッシュの存続期間をセットできます。

// Revalidate at most every hour
fetch('https://...', { next: { revalidate: 3600 } })

または、セグメント内のすべてのfetchリクエストの設定や、fetchが利用できない場合のために、Route Segment Config optionsを使うことができます。

Time-based Revalidationの動作

  • revalidate(再検証)と共に初回のfetchリクエストが呼ばれたら、データは外部データソースからfetchされ、Data Cacheに保存されます。
  • 指定された時間枠内(例: 60秒)に行われたリクエストはキャッシュされたデータを返します。
  • 時間枠が経過した後も、次のリクエストはまだキャッシュされた(古い)データを返します。
    • Next.jsはバックグラウンドでデータのrevalidation(再検証)をトリガーします。
    • データのfetchに成功したら、Next.jsはData Cacheを新しいデータで更新します。
    • もしバックグラウンドでrevalidation(再検証)が失敗したら、以前のデータが置き換わらずに保持されます

これはstale-while-revalidateのふるまいに似ています。

On-demand Revalidation(要求ベースの再検証)

データは、パス(revalidatePath)、またはキャッシュタグ(revalidateTag)によってオンデマンドでrevalidate(再検証)が可能です。

On-Demand Revalidationの動作

  • 初回のfetchリクエストが行われたら、データは外部データソースからfetchされ、Data Cacheに保存されます。
  • on-demand revalidationがトリガーされた時、適切なキャッシュエントリがキャッシュから削除されます。
    • これはtime-based revalidationと違います。time-based revalidationは新しいデータが取得されるまでキャッシュ内に古いデータが保持されます。
  • 次のリクエストが行われた時、再びキャッシュMISSとなり、データは外部データソースからfetchされ、Data Cacheに保存されます。

Opting out(除外、無効化)

個々のデータのfetchでは、cacheオプションをno-storeに設定することで、キャッシュを無効ににできます。これは、fetchが呼び出されるたびにデータが取得されることを意味します。

// Opt out of caching for an individual `fetch` request
fetch(`https://...`, { cache: 'no-store' })

または、Route Segment Config optionsを使うことで、特定のルートセグメントに対するキャッシュを無効にできます。これはサードパーティライブラリも含めて、ルートセグメント内の全てのデータリクエストに影響します。

// Opt out of caching for all data requests in the route segment
export const dynamic = 'force-dynamic'

Vercel Data Cache

もしNext.jsのアプリケーションがVercelにデプロイされているのであれば、Vercelの特徴を理解するために、Vercel Data Cache のドキュメントを読むことをお勧めします。

Full Route Cache

Next.jsはビルド時に自動的にルートのレンダリングとキャッシュをします。
これは最適化で、リクエストごとにサーバーでレンダリングする代わりにキャッシュされたルートを提供することで、ページの読み込みを高速化します。

Full Route Cacheを理解するためには、Reactがどのようにレンダリングを行うか、Next.jsがどのように結果をキャッシュするかを見ることが役立ちます。

1. サーバー上のReactレンダリング

サーバー上では、Next.jsはレンダリングを調整するためにReactのAPIを使用します。レンダリング作業は、個々のルートセグメントとSuspenseの境界ごとにチャンクに分割されます。

それぞれのチャンクは2つのステップでレンダリングされます。

  1. ReactはServer Componentsをストリーミングに最適化された特別なデータフォーマットにレンダリングします。これはReact Server Component Payloadと呼ばれます。
  2. Next.jsはReact Server Component PayloadClient Component JavaScript の命令を使って、サーバー上でHTMLをレンダリングします。

これは、作業をキャッシュしたりレスポンスを送信したりする前に、すべてがレンダリングされるまで待つ必要がないことを意味します。 代わりに、作業が完了するにつれてレスポンスをストリームできます。

React Server Component Payloadとは

React Server Component Payloadはレンダリングされた Reactサーバーコンポーネントツリーのコンパクトなバイナリ表現です。それはブラウザのDOMを更新するために、Reactによってクライアント上で使用されます。
React Server Component Payloadは下記を含みます:

  • Server Componentsのレンダリング結果
  • Client Componentsがレンダリングされるべき場所や、JavaScriptファイルへの参照のためのプレースホルダ
  • Server ComponentからClient Componentへ渡されるprops

詳細はServer Components

2. サーバー上のNext.jsのキャッシュ(Full Route Cache)

Next.jsのデフォルト動作はルートのレンダリング結果(React Server Component PayloadHTML)をサーバー上でキャッシュすることです。これはビルド時の静的レンダリングされたルートや、revalidation(再検証)の間に適用されます。

3. クライアント上のReact HydrationとReconciliation

リクエスト時、クライアント側では:

  1. HTMLは、ClientServer Componentsの非対話型の高速な初期プレビューを即座に表示するために使用されます。
  2. React Server Components Payloadは、ClientとレンダリングされたServer Componentsのツリーを調整し、DOMを更新するために使用されます。
  3. JavaScriptの命令は、 Client Componentshydrateし、アプリケーションを対話的にするために使用されます。

4. クライアント上のNext.jsのキャッシュ(Router Cache)

React Server Component PayloadはクライアントサイドのRouter Cacheに保存されます。これは個々のルートセグメントで分割された、個別のインメモリキャッシュです。
このRouter Cacheは、過去に訪れたルートを保存し、将来のルートをprefetchingすることで、ナビゲーションのエクスペリエンスを向上させるために使用されます。

5. 後続のナビゲーション

後続のナビゲーションやprefetching中に、Next.jsはReact Server Components Payload
Router Cacheに保存されているかを調べます。もし存在すれば、新しいリクエストをサーバーに送信せずにスキップします。

もしルートセグメントがキャッシュになければ、Next.jsはサーバーからReact Server Components Payloadを取得し、クライアント上のRouter Cacheにデータを格納します。

静的、動的レンダリング

ビルド時にルートがキャッシュされるかどうかは、そのルートが静的にまたは動的にレンダリングされるかどうかによります。静的ルートはデフォルトでキャッシュされます。動的ルートはリクエスト時にレンダリングされ、キャッシュされません。

下の図は、静的にレンダリングされるルートと動的にレンダリングされるルート、キャッシュされたデータとキャッシュされていないデータの違いを示しています。

詳細はstatic and dynamic rendering

Duration(存続期間)

デフォルトでは、Full Route Cacheは永続的です。これは、レンダリングのアウトプットがユーザーのリクエストをまたいでキャッシュされることを意味します。

Invalidation(無効化)

Full Route Cacheinvalidate(無効化)する二つの方法があります。:

  • Revalidating Data(データの再検証): Data Cacherevalidate(再検証)することで、サーバー上でコンポーネントを再レンダリングし、新しいレンダリングのアウトプットをキャッシュすることにより、Router Cacheが無効になります。
  • Redeploying(再デプロイ): Data Cacheがデプロイをまたいで永続するのとは異なり、Full Route Cacheは新しいデプロイ時にクリアされます。

Opting out(除外、無効化)

Full Route Cacheを無効にできます。言い換えれば、すべてのリクエストに対して動的にコンポーネントをレンダリングする方法は、以下のようになります:

  • Dynamic Functionを使用: これによりルートはFull Route CacheからOpting out(除外、無効化)され、リクエスト時に動的にレンダリングされます。Data Cacheは引き続き使用できます。
  • route segment config optionsのdynamic = 'force-dynamic' または revalidate = 0を使用: これはFull Route CacheData Cacheをスキップします。つまり、コンポーネントはレンダリングされ、サーバーに対するすべてのリクエストでデータはfetchされます。Router Cacheはクライアントサイドのキャッシュとして引き続き適用されます。
  • Data CacheのOpting out(除外、無効化): もしルートにキャッシュされてないfetchリクエストがある場合、そのルートはFull Route Cacheから除外されます。特定のfetchリクエストのデータは、すべてのリクエストごとに取得されます。キャッシュをOpting out(除外、無効化)しない他のfetchリクエストは引き続きData Cacheにキャッシュされます。これにより、キャッシュされたデータとキャッシュされていないデータのハイブリッドが可能となります。

Router Cache

Next.jsはユーザセッションの間、React Server Component Payloadを個々のルートセグメントごとに分割して保存するインメモリのクライアントサイドのキャッシュを持ちます。

Router Cacheの動作

ユーザーがルート間をナビゲートする時、Next.jsは訪れたルートセグメントをキャッシュし、ユーザーがナビゲートする可能性のあるルートを(ビューポート内の<Link>コンポーネントに基づいて)prefetchingします。

これにより、ユーザーは改善されたナビゲーション体験が得られます:

  • 訪れたルートがキャッシュされているため、即座の戻る/進むナビゲーションが可能であり、prefetchingとpartial renderingにより新しいルートへの高速なナビゲーションが行えます。
  • ナビゲーション間にはフルページの再読み込みがなく、Reactの状態とブラウザの状態が保持されます。

Router CacheとFull Route Cacheの違い:

Router CacheReact Server Component Payloadをユーザーセッションの間に一時的にブラウザーに保存します。
一方、Full Route CacheReact Server Component PayloadHTMLを複数のユーザーリクエストをまたいで、永続的に保存します。

Full Route Cacheは静的にレンダリングされたルートのみをキャッシュしますが、Router Cacheは静的にも動的にもレンダリングされたルートの両方に適用されます。

Duration(存続期間)

キャッシュはブラウザのメモリに保存されます。2つの要素がキャッシュの存続期間を決定します:

  • Session: キャッシュはナビゲーションをまたいで存続します。しかし、ページをリフレッシュするとクリアされます。
  • Automatic Invalidation Period: 個々のセグメントのキャッシュは特定の時間が経過すると自動的に無効になります。期間はルートが静的にレンダリングされたか、動的にレンダリングされたかによって決まります。:
    • 動的レンダリング: 30 秒
    • 静的レンダリング: 5 分
      ページのリフレッシュはすべてのキャッシュされたセグメントをクリアしますが、自動的な無効化期間は、最後にアクセスまたは作成された時点から個々のセグメントにのみ影響します。

動的にレンダリングされたルートに対してprefetch={true}を追加するか、またはrouter.prefetchを呼び出すことで、そのルートを5分間キャッシュするようにできます。

Invalidation(無効化)

Router Cacheを無効にする方法は2つあります。:

  • Server Actionの中で:
    • パス(revalidatePath)による、またはキャッシュタグ (revalidateTag)によるデータの再検証の要求
    • cookies.setまたはcookies.deleteを使用すると、クッキーを使用するルートがstaleとなることを防ぐために、Router Cacheが無効になります(例:認証).
  • Calling router.refreshを呼び出すと、Router Cacheが無効化され、現在のルートに対して新しいリクエストがサーバーに送信されます。

Opting out(除外、無効化)

Router CacheをOpting out(除外、無効化)することはできません。しかし、
router.refreshrevalidatePathrevalidateTagを呼ぶことでinvalidate(無効化)できます。 これによりキャッシュがクリアされ、サーバーへの新しいリクエストが行われて、最新のデータが表示されます。

<Link>コンポーネントのprefetchプロパティをfalseに設定することで、prefetchingopt outすることもできます。ただし、タブバーや戻るや進むナビゲーションなど、ネストされたセグメント間の迅速なナビゲーションを可能にするために、ルートセグメントが一時的に30秒間保持されます。訪れたルートは引き続きキャッシュされます。

キャッシュの相互作用

異なるキャッシングメカニズムを構成する時には、それらがどのように互いに影響するかを理解することが重要です。

Data Cache と Full Route Cache

  • レンダリングのアウトプットはデータに依存するため、Data CacheRevalidating(再検証)やopting out(除外、無効化)はFull Route Cacheinvalidate(無効化)します。
  • Full Route CacheInvalidating(無効化)やopting out(除外、無効化)はData Cacheに影響しません。キャッシュされたデータとキャッシュされていないデータの両方を持つルートを動的にレンダリングできます。これは、ページの大部分がキャッシュデータを使用しているが、リクエスト時にフェッチする必要があるデータを依存するいくつかのコンポーネントがある場合に役立ちます。すべてのデータを再度フェッチするパフォーマンスの影響を気にせずに動的にレンダリングできます。

Data Cache と Client-side Router cache

  • Route HandlerData Cacherevalidating(再検証)をしても、Route Handlerは特定のルートに結び付いていないため、Router Cacheはすぐにはinvalidate(無効化)になりません。 これはハードリフレッシュが行われるか、automatic invalidation period(自動的な無効化期間)が経過するまで、Router Cacheが以前のペイロードを提供し続けることを意味します。

  • Data CacheRouter Cacheを即時にinvalidate(無効化)にするためには、Server Action内でrevalidatePathまたはrevalidateTagを使用します。

Discussion