はじめに
Next.jsには現在4つのキャッシュがあります。
- Request Memoization
- Data Chache
- Full Route Cache
- Router Cache
そして、現在のv15でのカナリー版ではuse cache
が追加され、宣言的なcachingが可能となりました。
これはData CacheやFull Route Cacheの機能等のcachingを宣言的に行うことが可能な新しいdirectiveです。
今回は、それぞれに対して、仕組みや注意点などの部分まで解説を行いたいと思います。
4つのcache概要
概要ですが、公式に概要を示す表があったので、それをわかりやすく訳したものを記載します。
すると、以下のようになります。
機能 | cacheされるもの | 保存場所 | 目的 | cache期間 |
---|---|---|---|---|
Request Memoization | Return values of functions | Server | React コンポーネントツリーでデータを再利用する | リクエスト毎 |
Data Cache | Data | Server | ユーザーのリクエストとデプロイメント全体にわたってデータを保存する | 永続的(再検証可能) |
Full Route Cache | HTML and RSC payload | Server | レンダリングコストを削減し、パフォーマンスを向上 | 永続的(再検証可能) |
Router Cache | RSC Payload | Client | ナビゲーション時のサーバーリクエストを削減 | ユーザーセッション または time-based(時間ベース) |
次に、各cacheについて、より詳細に見ていきましょう。
Request Memoization
これは同じリクエストを重複排除するというcachingです。
以下の画像のように、Next.jsでは同一リクエストを自動でメモ化します。
仕組み
仕組みとしては、初回リクエストは後述するData Cache
もしくは、DBやServerにリクエストをし、通常通りfetchします。そして、受け取ったレスポンスをin-memory
に保存します。
2回目以降のリクエストは、同じレンダリングパスの中で同じリクエストを呼び出した場合に、in-memory
を参照し、メモ化したものを使用するといったことをします。
ルート(/about
などの各segment(ページ)のこと)がレンダリングされ、レンダリングパスが完了すると、in-memory
がリセットされ、すべてのリクエストのメモ化エントリがクリアされます。
つまり、Request Memoizationの目的は重複リクエストの排除によるリクエスト数の削減と言えます。
少し耳慣れない用語もでてきたので、以下にまとめます
- レンダリングパス:ルートのレンダリングが開始されてから完了するまでの一連のプロセス
- メモ化エントリ:リクエストの結果をcachingしたデータの記録のこと
つまり、特定のパスのリクエストにおいて、レンダリング中はRequest Memoizationが発火するが、レンダリングが完了するとcache purgeされるため、リクエスト毎に1回は外部ソースにfetchリクエストが走るという仕組みです。
上記の仕組みがあるからこそ、Next.jsでは積極的にコロケーションを行い、リーフコンポーネントにfetch処理をカプセル化することができます。
(動的メタデータの設定の際には、generateMetadata()
を使用しますが、その関数内でのfetch()
に対しも有効です)
注意点
- Server Componentでは
fetch()
を使用する場合に適用
-fetch()
のURLやオプションが少しでも違うとメモ化されない - ORMなど
fetch()
以外を使用する場合は、React.cacheを使用する - RouteHandlers(API)での
fetch()
利用では適用されない -
server-only
パッケージーを積極利用する
- ServerComponentでないとRequestMemoizationは効力を持たない
キャッシュ期間
リクエスト毎(各fetch毎)に行われます。
Data Cache
概要は以下の記事がかなり正確なので、引用しようと思います。
Next.jsでは、fetchした結果をData Cacheとして保存します。fetchによりデータソースにリクエストした結果はData Cacheに保存され、以降同一リクエストはData Cacheから返されます。つまりData Cacheの役割は、オリジンデータソースへのリクエスト数を減らすことです。
また、Data Cacheは永続的で複数デプロイ間にわたって有効なので、再デプロイしてもキャッシュは破棄されません。なので一定期間や特定のタイミングでキャッシュを破棄するためにfetchオプションなどによる再検証が用意されています。先ほどのReact Cacheはレンダリングごとに自動的にキャッシュが破棄されましたが、キャッシュされる期間が大きく異なるので混同しないように注意しましょう。
重要なのは以下の部分です。
- fetchした結果をData Cacheとして保存すること
- Data Cacheの役割は、オリジンデータソースへのリクエスト数を減らすこと
- Data Cacheは永続的で複数デプロイ間にわたって有効なので、再デプロイしてもキャッシュは破棄されないこと
- 上記のため一定期間や特定のタイミングでキャッシュを破棄するためにfetchオプションなどによる再検証が用意されていること
より詳しく見ていきましょう。
仕組み
Data Cacheの仕組みですが、force-cache
によるfetchにて、リクエストされると、まずはData Cacheを見に行きます。
そこで、cacheがない場合は、オリジンデータソースにリクエストが行われ、結果がData Cacheに保存され、メモ化されます。
cacheが見つかった場合は、cacheをすぐに返すというような仕組みです。
つまり、Data Cacheの目的はオリジンデータソースへのリクエスト数の削減と言えます。
上述の通り、これはfetch()
のオプションを利用して、cacheを制御するというようなメカニズムになっております。
no-store
を指定すれば、Data Chaceの参照はスキップされ、毎回オリジンデータソースにリクエストされることになります。
そのため、revalidate(再検証)の指定も可能です。
再検証については後述します。
キャッシュ期間
永続的です。ただし再検証の指定が可能です。
再検証
再検証には2種類あります。
- Time-based Revalidation:一定の時間が経過後に、新規リクエストが行われた後に、データを再検証する
- On-demand Revalidation:フォーム送信などのイベントに基づきデータを再検証する
Time-based Revalidation
1.はいわゆるISRのことです。
これはSWRの挙動を取るため、再検証後の初回リクエストのみ古いデータを返却します。
つまり、新しいリクエストの後、古いデータを返しつつ、その裏で最新のデータを取得し、Data Cacheにセットするというような挙動をしています。
On-demand Revaldation
2.はrevalidatePath()
やrevalidateTag()
による再検証を指します。
これは、指定したパスもしくはタグに関連するキャッシュエントリをすべて削除します。
そして、ISRと異なり、次のリクエスト時には新しいデータをオリジンデータソースから取得し、Data Cacheにセットするという動きを取ります。
注意点
また、Data Cacheは永続的で複数デプロイ間にわたって有効なので、再デプロイしてもキャッシュは破棄されません。なので一定期間や特定のタイミングでキャッシュを破棄するためにfetchオプションなどによる再検証が用意されています。
Data Cacheは永続的なので、サーバーを再起動したり、再デプロイしたりしても残ります。
(もちろんタブを閉じるなどの行為をしても残ります)
なので、以下の記事にある通り、build時に.next/cache/fetch-cache
を削除し、再デプロイをすることでキャッシュを破棄したうえで、デプロイすることが可能です。
個人的には"build:clean": "rm -rf .next && next build"
というコマンドをプロジェクト立ち上げ時に追加しておくことをおすすめします。
Full Route Cache
Next.jsではbuild時または再検証時に静的ページ(静的にレンダリングされたルート)に対して、サーバー側でcachingを行います。
つまり、SSG・ISRで生成されたページをサーバー側でcachingするということです。
具体的には、HTMLとRSC Payloadをサーバー側でcachingします。
RSC Payloadとそのcacheについての概要は以下となります。
- RSC Payload:レンダリングされたReact Server Components Treeのバイナリ
- コンポーネントのTree構造や必要なpropsなどの情報をバイナリー化しReactがcachingする
以上が概要ですが、少し曖昧なので、仕組みの方でより詳しく解説します。
仕組み
Full Route Cacheでは、HTMLとRSC Payloadをbuild時または再検証時にcachingします。
そして、Next.jsではサーバー側でRSC PayloadとHTMLをレンダリングします。
つまり、静的なルート(ページ)にクライアントサイドでアクセスした場合、Data Cache や Request Memoization などのサーバーで行うリクエストや処理はすでに完了している 状態になります。
また、Full Route Cache によって RSC Payload と HTML はすでにレンダリング済み です。
そのため、クライアント側での作業は JS の Hydration(React によるインタラクティブ化) のみとなります。
つまり、Full Route Cache の目的は 静的ルート(ページ)自体をキャッシュし、不要なレンダリングやリクエストを削減する ことを目的としています。
簡単に言えば、レンダリングコストを削減し、静的なページを即座に表示させる ためのキャッシュです。
さらに、クライアント側には Router Cache が存在し、初回以降のページ遷移時にサーバーリクエストを減らすことで、さらなる高速化 が可能になります。
これにより、特に next/link
などを使ったクライアントサイドのページ遷移では、不要なサーバー通信を行わずにスムーズなナビゲーションが実現されます。
静的ルートと動的ルートでの挙動の違い
前述した通り、クライアント側ではRouter Cahceが存在しています。
そのため、静的ルートと動的ルートで挙動に違いが生まれます。
静的ルートの場合
初回アクセス時、Full Route Cache(サーバー側)のCacheを取得し、このときに、クライアントのin-memory
にRSC Payloadのみcachingします。
そして、ページ遷移時には、クライアントのin-memory cacheを参照することで非常に高速な遷移が可能となっています。(詳しくは後述しますが、処理がサーバー側までいかないので高速化が実現できます)
動的ルートの場合
Full Route Cacheの参照はスキップされ、Data Cacheを参照するなどのfetch等の処理に入ります。
そして、fetch等の処理後、クライアントのin-memory
にRSC Payloadをcachingします。
つまり、SSRやDynamic Routeなどの動的ルート(ページ)ではサーバー側にcachingはされないが、クライントにはcachingされるということです。
そして、ページ遷移については静的ルートと同様にクライアントのin-memory
を参照します。
こと、ページ遷移については静的か動的かは関係ないということです。
ただし、あくまでcachingされるのはRSC Payloadなので、動的ルートでData Cacheなどが存在するのであれば、サーバー側にまでリクエストが行われるということは覚えておいてください。
キャッシュ期間
永続的です。
build時および再検証時にcacheがセットされるので、どのユーザーに対してもcacheを返却します。
Router Cache
クライアントでRSC Payloadをcachingする機能です。
これは唯一クライアント側でのcacheとなるため、ページアクセス時にcachingが行われます。
これにより、フォワード / バックナビゲーション(戻る / 進むボタン)による高速な画面遷移を実現できるため、UXが大幅に向上します。
仕組み
アクセスしたページをクライアントのin-memory
にcachingします。
これは静的/動的問わず、cachingを行います。
また、next/linkの<Link />
コンポーネントにより指定したルートをprefetch
することができます。
これにより、ユーザーがルートにアクセスする前にバックグラウンドでルートをプリロードすることが可能となります。
つまり、ナビゲーション間でページ全体が再読込されることがなくなり、Reactおよびブラウザの状態を保持できるため、高速なページ遷移が可能となります。
※ prefetchはbuild時の環境のみに適用されるので、next dev
では確認できないということに注意が必要です
上記のため、Router Cacheの目的はナビゲーションのUX向上と言えます。
ちなみに、<Link />
のナビゲーションはソフトナビゲーションなので、差分があるルートセグメントのみレンダリングする部分レンダリングの方式となっています。
(従来のページ遷移は基本的にハードナビゲーションでページ遷移する度にクライアント全体がレンダリングされてしまいます)
静的ルートと動的ルートによるprefetchの挙動の差異
prefetchのデフォルトの挙動は静的か動的かで異なります。
静的ルートの場合
デフォルトでprefecthがtrue
となっており、30秒間 Router Cacheにcachingされます。
また、preloadのタイミングについては以下のタイミングで、preloadが行われます。
- ユーザーのViewPortに
<Link />
が入った時(スクロールして見えた時)
これが静的ルートの場合の挙動です。
動的ルートの場合
動的ルートの場合は、デフォルトで共通レイアウト(静的な部分)とloading.js
がprefetchされます。
ただし、<Suspense />
のfallback
はprefetchされないので、混同に注意が必要です。
つまり、動的ページはlayout.js
などの共通レイアウトと、loading.js
のフォールバックUIをRouter Cacheに保存することで、動的ルート全体のfetchコスト(レンダリングコスト)を削減し、UXを向上しているわけです。
そして、最終的には動的ページはRouter Cacheに保存されるので、以降のアクセス時にはRouter Cacheを参照し、動的ページのRSC Payloadを即返却するため、同じページであれば、即画面表示できるということになります。
キャッシュ期間
セッション中(ブラウザのタブを閉じるまで)のみ有効です。
また、静的ルートの場合は、デフォルトで5分間Cacheを保持しますが、動的ルートの場合は、0秒です。
ただし、Router Cacheの場合、next.configのstaleTImes
により詳細に時間を決められます。
なので、基本デフォルトを使用し、必要があれば、設定を編集することになると思います。
また、オプトアウトについては、以下の方法があります。
- Server Actionsで以下を行う
a. revalidatePath() / revalidateTag()
b. cookies.set() / cookies.delete() - router.refresh()
以上の方法でcacheをオプトアウトし、ルートの最新化が可能となるので、これも覚えておきましょう。
use cache
これはNext.js v15から使用可能となったuse cache
ディレクティブという方法です。
(現在はカナリー版でのみ使用可能です)
これは以下のような経緯から作成された機能です。
- Next.jsではWeb標準のfetchを拡張しており、デフォルトのfetchとの違いがある
- Data Cacheの最適化が難しい
- ここにRoute Segment Configも絡んでくるため、より難しい
このような経緯があり、それなら、今までのCache指定方法はすべて忘れて、use cache
ディレクティブ1つにしてしまおうというようなものがuse cache
です。
(ただし、Data Cacheとの関連性が強いので、その他のcacheについては覚えておく必要があります)
要するに、よりCache最適化しやすいように作られた機能と言えます。
より具体的に知りたいという方は以下の記事をご参照ください。
使用方法
以下のように各Levelで宣言・cachingが可能です。
// File level
'use cache'
export default async function Page() {
// ...
}
// Component level
export async function MyComponent() {
'use cache'
return <></>
}
// Function level
export async function getData() {
'use cache'
const data = await fetch('/api/data')
return data
}
もちろん、revalidateTag()やISRのようなOn-demandなCacheとすることも可能です。
これにはcacheTag()
とcacheLife()
を使用します。
import { cacheTag } from 'next/cache';
async function getNotice() {
'use cache';
cacheTag('my-tag');
}
"use cache"
import { unstable_cacheLife as cacheLife } from 'next/cache'
export default async function Page() {
cacheLife("minutes")
return
}
cacheLife()
の設定値は以下を指定できます。
- "seconds"
- "minutes"
- "hours"
- "days"
- "weeks"
- "max"
注意点
use cahce
の使用ではいくつか注意点があるので、そちらについても記載しておきます。
cache kyeの自動生成
use cache
ではcache-keyが自動生成されます。
関数などに渡した引数を自動で検出して、生成されるようなのですが、この値はシリアライズ可能な値でないといけないです。(シリアライズ不可な値やクローズドオーバーされた値(クロージャで定義された変数)を引数等で使用してもcache-kyeの生成に使用されないだけなのでエラーとはなりません)
cache可能な値について
use cache
使用時には、cacheされる値(関数の戻り値など)はシリアライズ可能である必要があります。
シリアライズ可能な値でないとエラーとなります。
逆に言えば、Reactがレンダリングできるシリアライズ可能なあらゆるデータをcaching可能ということです。
ここがunsatable_cache
と異なる点で、コンポーネントのレンダリング出力を含めて、Reactがシリアライズ可能な値であれば、すべてcachingできます。
おわりに
いかがだったでしょうか。
現状のNext.jsのすべてのcache機能について網羅的に解説できているかと思います。
不明点や誤り等あれば、コメントいただけると幸いです。
また、本記事がNext.jsの理解に少しでも寄与する部分があればと願っております。
参考文献