Next.jsのApp Routerが提供する4つのキャッシュ機能

こんにちは。 新規プロダクト開発に携わっているエンジニアの島田です。

今年2023年の5月にNext.js 13.4がリリースされました。このリリースで、これまでのアーキテクチャ (今ではPages Routerと呼ばれるようになりました) を大幅に刷新した、App Routerが安定版となりました。
上記リンクにある通り非常に情報量の多いリリースとなり、果たしてこのApp Routerを現時点で採用するべきなのかどうかについて、多くのプロジェクトで関心が高まっているかと思います。

この記事ではその採用可否の判断の一助になるよう、App Routerが提供する既存のものから大幅に刷新されたキャッシュ機能について、それぞれの概要に加えて開発を進める上で便利な点や注意点を解説していきます。

4つのキャッシュ機能

App Routerのキャッシュ機能は4つの種類に大別されます。以下それぞれ見ていきます。

Request Memoization

Next.jsのfetch関数等を用いて出されたリクエストは、画面描画のための一連の処理が終わるまでキャッシュされます。これをRequest Memoizationと呼びます。

例えばSNSの画面を実装することを想定してみてください。画面にはヘッダーやチャット画面等の複数のコンポーネントが存在しており、そこにはユーザーの名前が計3つ別々の場所に表示されているとします。
名前を呼び出すAPIに何度もリクエストを出す必要は無いので、一度だけ取得したら後は画面内でそれを使い回すようにしたいかと思います。しかし、単純にPropsとして渡して行こうとすると、名前を必要とするコンポーネントがそれぞれ全然違う場所にある場合いわゆるProp Drillingが発生してしまいますし、グローバルStateとしてどこからでも呼び出せるようにしてしまうのも、メンテナンスの観点から適当かどうか判断するのはそう簡単では無いかもしれません。

App Routerを採用するNext.jsアプリケーションでは、Request Memoizationがあることで、コード上では同じAPIを各所で呼ぶように記述するだけでリクエストの数は一回だけになり、そのレスポンスが使い回されるようになります。
処理の流れは次の図のようになります:

Next.js公式サイトから拝借

これにより実装の手間が減るのはもちろん、かつてFacebookがFlux Architectureを発明する契機となった例のような、コンポーネント及びそれが参照するデータが多いが故に起こる意図しない挙動も発生しづらくなって、メンテナンス性の向上が期待されます。

尚、Request Memoizationにより生成されたキャッシュは画面描画が終わると破棄されます。従って次回以降のアクセス時にもキャッシュしたデータを参照したい場合、次に説明するData Cacheを使うことになります。

Data Cache

Data cache はNext.jsのfetch関数等を用いて出されたリクエストのレスポンスに対するキャッシュを指します。
どのような挙動となるか、また前述のRequest Memoizationとの関係は次の図を参考にイメージしていただければと思います:

Next.js公式サイトから拝借

いきなり注意点となりますが、明示的に設定しなければこのキャッシュは残り続けます。これがバグの温床になりかねないので、fetch関数を用いた実装をするときは、次に示すRevalidationやOpt outの設定を毎回考慮することになるかと思います。

Revalidation

Revalidation (キャッシュを破棄することでデータ取得を再度行うようにすること) は、制限時間の設定や、所定のパスやタグをターゲットとするオンデマンドな処理の実行で実現することになります。

まず時間ベースで設定する場合は、fetch関数に次のようなオプションを付与します:

// 1時間でRefalidationが行われるようにオプションを付与
fetch('https://...', { next: { revalidate: 3600 } })

これにより、1時間後にはキャッシュヒットしなくなってAPI呼び出しが行われるようになります。

また、fetch関数にはオプションとして次のようにタグを設定することも出来、所定のタグが設定されたキャッシュに対してrevalidateTag関数を実行することで、オンデマンドにRevalidationを実施することも可能です:

// これによって得られるレスポンスに a, b, c 3つのタグを付与
fetch(`https://...`, { next: { tags: ['a', 'b', 'c'] } })

// a というタグがついているキャッシュをRevalidate (これで上記のfetch関数で得られたキャッシュが破棄される)
revalidateTag('a')

またオンデマンドなRevalidationを行う関数として、パスを対象とするrevalidatePath関数も存在します。これは指定したパスに紐づくData CacheをRevalidateするものになります。

これらいずれかのRevalidationが実行されると、その対象を含むRoute (特定のパスに紐づくページ)に対応する、後述のFull Route Cacheも合わせて失効されます。

Opt out

そもそもキャッシュを生成させないようにすることをOpt outと言います。Data cacheに対するOpt outは、fetch関数に次のオプションを付与することで設定できます:

fetch(`https://...`, { cache: 'no-store' })

あるRouteがOpt outするよう設定されているfetch関数を含む場合、そのRouteでは、後述のFull Route CacheもOpt outされる点に注意してください。

Full Route Cache

上述の通り、Next.jsではある特定のパスに紐づくページをRouteと呼びます。
このRouteはビルド時にレンダリングされ、その結果はキャッシュされます。このキャッシュがFull Route Cacheです。これはData Cacheと同じく明示的に失効させたり、Opt outの設定 (そもそもキャッシュを生成させないようにすること) をしない限り残り続けます。

これを失効させるには、対象のRouteに含まれるData CacheのどれかをRevalidateするか、Next.jsアプリケーションを再ビルドさせる必要があります。 そもそも生成させないように設定するのは提供されているいくつかのOpt out設定を行うか、対象のRouteに含まれるData CacheのどれかをOpt outすることで実現出来ます。

このキャッシュがあることによってパフォーマンスが落ちるといったことが無いように結構な工夫がされているので、描画速度等のためにOpt outするかどうかの判断に迷った場合は、公式ドキュメントにある処理の流れをご一読の上判断されることをおすすめします。

Router Cache

クライアント端末に残される、アクセスしたRouteのキャッシュをRoute Cacheと言います。これはページのリフレッシュか、30秒もしくは5分間 (設定によります) の時間経過により破棄されます。
これがあることで、同じアプリケーション内での2回目移行のページ遷移時にほぼレンダリングやデータ取得処理を行われなくなり、ユーザー体験が良くなることが期待されます。
挙動は次の図のようになります:

Next.js公式サイトから拝借

しかしこれも注意が必要で、実はRoute CacheはInvalidate (失効させる) ことは出来ても、Opt out (そもそも生成されないようにすること) は出来ません。ですので、同じアプリケーション内での遷移に関しては、何かしらの工夫を都度行わなければ最低でも30秒間キャッシュされた値が表示され続けてしまうという点を理解した上でアプリケーションの設計を行う必要があります。

尚、Next.js側にあり、明示的な設定や処理が行われない限り残り続けるFull Route Cacheとは性質が異なる点に注意してください。両者の関係は次の図を見てもらうとイメージしやすいかもしれません:

Next.js公式サイトから拝借

さいごに

ここまでApp Routerのキャッシュ機能の概要を説明しました。全容を把握するとっかかりの記事として、情報を厳選して読みやすくしたつもりではある一方、それによって誰かにとって必要な情報が抜け落ちている可能性も否定出来ません。ですので実際に最終的な技術的判断を下す前に、以下に示す参考資料の内容を把握することをおすすめします。

この記事が誰かの仕事の一助となることを願っています。

参考資料