はじめに
WEBアプリを実装する際、キャッシュをどう取り扱うかでパフォーマンスに影響するのはご認識のとおりかと思います。
Next.jsにおいても4種類のキャッシュ定義がありますが、公式や参考記事を読んでもどうもすんなり腹落ちしない。
けどプロダクトでは意識していかないといかない。。。
なんて状態の方を想定し、本当にこれ以上ないくらい噛み砕いて説明し、実際に確認した結果を含めて共有できたらとの思いを記事にしてみました。
これを読んでわかること
- キャッシュの最低限の仕組みと意識すべき点
- 静的レンダリングと動的レンダリングの確認の仕方
注意
ここでは技術的な話よりとにかく実務ベースの考えを優先した記載としています。
理解をより深めるためにこの記事を叩き台に公式ドキュメントを一度読むことをお勧めします。
参考にしたもの
- akfm_satoさんのNext.jsの考え方という記事
- Next.jsの公式ドキュメントのキャッシュの部分
4種類のキャッシュ
4種類のキャッシュは以下の表のとおりです。
公式ドキュメントの表を掲載しようかと思いましたが、和訳されているNext.jsの考え方の記事内の表の方がわかりやすいのでこちらを使わせていただきました。
以降、一つずつ説明したいと思います。
キャッシュ1 Request Memoization(サーバー側キャッシュ)
用途
現在のNext.jsの実装ではコロケーションといって、コンポーネント単位でデータ取得を行いpropsのバケツリレーを行わないことが主流となっています。
そうなると、それぞれのコンポーネントで同一エンドポイントへのデータフェッチが複数行われることになり、リクエスト回数の増加によるパフォーマンス低下が懸念されます。
これを解決するのが、「Request Memoization」で、リクエストを行う関数をメモ化することでリクエストの重複実行を回避してくれます。
実装で気をつけるべきこと
Request MemoizationはReactの機能で自動的にメモ化してくれるため、特に実装上で意識することはありません。
使い回しも兼ねてデータフェッチ用の関数を作成し、各コンポーネントから呼び出すようにすれば良いかと思います。
落とし穴
「実装上で意識することはありません」としましたが、きちんとメモ化されるには諸条件があります。
以下は公式のGood to know:を和訳したものですが、メモ化したつもりがされていない、なんてことがないように挙動確認は必要です。(実際私がそうでした。後述します。)
- リクエストのメモ化はReactの機能であり、Next.jsの機能ではありません
- GETメソッドのメモ化はリクエスト内のfetchメソッドにのみ適用されます
- メモ化はReactコンポーネントツリーにのみ適用されます。つまり、次のようになります。
generateMetadata、generateStaticParams、Layouts、Pages、その他のServer Componentsのフェッチリクエストに適用されます - fetchメソッドが適切でない場合(一部のデータベースクライアント、CMSクライアント、GraphQLクライアントなど)には、Reactキャッシュ関数を使用して関数をメモすることができます
キャッシュ2 Full Route Cache(サーバー側キャッシュ)
用途
一昔前のSSG(ビルド時のみのレンダリング)/ISR(再検証時のレンダリング)相当の挙動。
静的レンダリングともいいます。(以後静的レンダリングとします。)
ビルド時にルートをレンダリングしてキャッシュすることで、リクエストごとに呼び出されているのに比べてページの読み込みが速くなります。
実装で気をつけるべきこと
デフォルトで静的レンダリングになりますが、実装の仕方によって次項のData Cache(動的レンダリング)へ自動的に切り替わるので注意が必要です。
キャッシュ3 Data Cache(サーバー側キャッシュ)
用途
一昔前のSSR(ユーザーリクエストごとのレンダリング)相当の挙動。
動的レンダリングともいいます。(以後動的レンダリングとします。)
ユーザーごとに表示内容が変わる画面(ログイン後のユーザーの管理画面)などは動的レンダリングが必要となります。
実装で気をつけるべきこと
動的レンダリングと判定された場合、本当に動的レンダリングでしか実装ができないか確認が必要かと思います。
静的レンダリングと動的レンダリングの判定
自動的に判定されますが、静的レンダリングをデフォルトとした上で、Dynamic Functionsと呼ばれる実装を用いると動的レンダリングとなります。
主なDynamic Functions判定実装
- cookies(ブラウザのクッキー情報)
- searchParams(ページのクエリパラメータを用いた実装を行う)
- fetchメソッドの第二引数に"no-store"を設定(キャッシュの強制無効)
キャッシュ4 Router Cache(クライアント側)
用途
4種類のキャッシュの中で唯一のクライアント側キャッシュ。
ルート間の移動操作に備え、あらかじめルートのプリフェッチを行なっておくことでナビゲーション間でページ全体を再読み込みすることを回避します。
実装で気をつけるべきこと
Router Cacheは画面上のデータ表示に関係します。
データベースへの更新後即座に画面反映させたい時などはRouter Cacheによって期待した動作にならない可能性があります。
その際は以下の実装によってRouter Cacheを無効化する必要があります。
- fetch実行時にrevalidateを設定する
- サーバーアクション関数にrevalidateTag/revalidatePathを設定する
- router.refresh()を設定する
※Router Cacheの設定については様々な考えがあるようなので今回は割愛します。
キャッシュの動作を実際に確認してみる
では、上のキャッシュがどのように作動するか確認していきたいと思います。
Router Cacheは正直どう確認するのが良いかわからないのでご容赦ください。
残るRequest Memoizationと静的レンダリングと動的レンダリングの区別について確認してみたいと思います。
Request Memoizationを実際に確認してみる
まずはRequest Memoizationです。
確認方法
- ルート直下で同じデータフェッチ関数を呼び出すために2つのコンポーネントを用意する
ホームページ直下のreturn文内に同じ記載内容でデータ取得を呼び出すProductsとProductsdemoを用意しました。
2. それぞれのコンポーネントから、フェッチを伴う関数を呼び出す
今回はlistProductsという関数でmicroCMSの商品情報を呼び出しています。
その中で動作確認用にconsole.log("Request Memoization check!");を含めています。
3. ビルドしてスタートコマンドで画面を表示させる
想定どおりならターミナルに表示される"Request Memoization check!"は一度限りのはずですが・・・
なぜか2回表示させます。
当初原因はわかりませんでしたが、今回はmicroCMSのフェッチメソッドを使っていてfetchメソッドを使っていなかったため、公式の説明のとおりメモ化されていない模様でした。
こういう場合はReactキャッシュ関数を使わないといけないようです。
メモ化できているつもりでできていないだとパフォーマンスが全く向上しないため、実際に動作確認は必要ですね。
※なお、キャッシュ関数でlistProductsを以下のようにラップしたところ、"Request Memoization check!"の表示は一度限りとなり、意図した挙動になりました。公式ドキュメント、大事です。
静的レンダリングと動的レンダリングの区別を実際に確認してみる
続いて、静的レンダリングと動的レンダリングがどう区別されるか確認してみたいと思います。
方法は簡単で、日頃npm run build/yarn buildなどのコマンドでビルドします。
すると、以下のような画面がVSCodeのターミナルに表示されるはずです。
上の画像からわかること
上のサンプルでは個人開発しているECサイトなのですが、ほとんどがDynamic=動的レンダリングになっているかと思います。
ECサイトという都合上、ユーザーごとに異なる画面(ユーザー情報、お気に入り、購入履歴)などが動的レンダリングになるのはやむを得ませんし、
逆にユーザー認証を必要としていない買い物かごページ(/cart)がStatic=静的レンダリングになるのも納得です。
また、ユーザー認証を伴わないサイトのホームページ(/)が動的レンダリングになっているのですが、ここはページ直下のコンポーネントでsearchParamsを使っているからなので、これも想定どおりの挙動です。
こちらは想定したとおりの挙動で一安心です。
ただ、圧倒的に動的レンダリングページの多く、これを何とかすべきなのかというのは次のステップで考えるべきなのかもしれません。
まとめ
改めてキャッシュは一朝一夕で完全にマスターできるものではなく、まだまだ未熟だと痛感されられました。
また、実際に動作確認をして分かったこともあり、失敗を恐れずに実際に手を動かして確認することも重要だと感じました。
拙い説明で大変恐縮ですが、私と同じような境遇の方にとって一助になれば幸いです。