目次
はじめに
今回は Next.js の ISR について調べてみました。
特に新しい話でもないですが、自分の理解が足りていなかったり SSR,SSG だけわかってればよくない?という甘えがあって、なかなか踏み込めていない場所だったので、ただの個人的なまとめになりそうですが、ご容赦くださいませ...
Next.js のページレンダリングの種類
CSR
クライアント側でレンダリングを行う手法。
クライアント側で JavaScript を処理して HTML を生成するので、処理を行う量が多くなるほどレンダリングに時間がかかってしまい表示速度が遅くなる。
利用シーンとしては以下のような場合に向いているようですが、経験が浅いので具体的な利用シーンの想像はついていないですね。
静的コンテンツを含むスケルトン ページを表示し、データのフェッチに長い待機時間を必要とするページにデータを徐々に挿入する場合に便利です。
https://guydumais.digital/blog/next-js-the-ultimate-cheat-sheet-to-page-rendering/
今のところ seo 的にもよくないのかも?という話もあります。
seo の詳しいことはわかりませんが、現状の Google のクローラの仕組みでは、CSR でレンダリングされたコンテンツが検索結果の上位に表示されるのに時間がかかりすぎてしまうようです。
SSR
サーバー側でレンダリングを行いクライアント側に HTML を返す手法。
CSR であったような表示が遅れたりするような問題は解決されます。
ただ、ユーザーからのリクエストごとにサーバー側で処理が行われるからオーバーヘッドが発生してパフォーマンスが低下する可能性があるとのことで、あまり推奨されていない手法のようですね。
Next.js では、静的コンテンツがビルド (事前レンダリング) され、リクエストごとにのみ配布されるため、最高のパフォーマンス結果を得るために、この戦略をできるだけ使用しないことをお勧めします。
https://guydumais.digital/blog/next-js-the-ultimate-cheat-sheet-to-page-rendering/
となると、SSR の特徴である初期描画もリアルタイム性も実現させたい場合にはどうしたらいいのか?と思って調べてみると、SWR で SSG or SSR を組み合わせて使用するという手法があるようですね。
https://zenn.dev/a_da_chi/articles/d590a69e6991d2
SSG
ビルド時にレンダリングが行われるため、CSR や SSR であったようなユーザからのリクエストごとにレンダリングを行う必要もないので速度が遅くなるといった問題はありません。
ユーザーに見せるのは、すでにビルド時に構築された HTML だからコンテンツの表示速度が早いという特徴があります。
ただ、コンテンツが大量にある場合には少し工夫する必要があります。
- 複数人のユーザーがコンテンツを追加するサービスなどで、すべてのページでデータの受け取りとレンダリングをしていたら時間がかかりすぎる
- どのタイミングでビルドすればいいかわからないため、ビルドとビルドの間にコンテンツの更新があった場合に対応できない
そういった課題へのアプローチとして getStaticPaths の fallback というプロパティがあります。
fallback: true
とすると、データ取得に関する箇所以外がレンダリングされた静的なページに fallback(機能や性能を制限して動かすこと) してくれます。
つまり、ビルド時にデータ取得とレンダリングが行われないページでユーザからのリクエストがあった場合にはプリレンダリングはされている状態になります。
流れとしては以下のようになります。
- 1.クライアント側に取得を要するデータが欠けている状態の HTML がサーブされる
- 2.クライアント側と同時にサーバー側でも必要なデータの取得とレンダリングが行われる
- 3.それ以降のユーザーからのリクエストに対してすでにサーバー側で生成されたキャッシュを返す
fallback: 'blocking'
にすると、ture の時にデータの取得がなされていない状態の不完全な HTML がクライアント側にサーブされてしまうという問題を解決できます。
ture の場合、データの取得がなされていない不完全な HTML を返していましたが、'blocking'は文字通りこの挙動をブロックして、不完全な HTML のまま返すことはありません。
ユーザーからはじめてリクエストがあったタイミングで、クライアント側ではなくサーバー側でデータの取得とレンダリングが行われるため、完全な状態の HTML をクライアント側にサーブすることができます。
そうすることで、ユーザーに完全な HTML を表示することができて、初回のアクセス時にもレンダリングが完成された状態で表示することが可能です。
- true はクライアント側でレンダリング
- "blocking"はサーバ側でレンダリング
ちなみに、インフラに vercel を利用する場合 Vercel Edge Network という CDN が備えついているため、vercel にデプロイすると自動的にコンテンツがキャッシュされてユーザーに高速に表示できるようですね。
https://vercel.com/docs/concepts/edge-network/overview
レンダリング手法の使い分け
すごくシンプルに表すとこんな感じになりそうですね。
- CSR: ❌ 初期描画 ⭕️ リアルタイム性
- SSR: ⭕️ 初期描画 ⭕️ リアルタイム性 ❌ パフォーマンス
- SSG: ⭕️ 初期描画 ❌ リアルタイム性 ⭕️ パフォーマンス
https://www.gaji.jp/blog/2022/08/08/10715/
そのうち、どういった場面にそれぞれのレンダリング手法が適しているのかも記事にしていきたいですね。
本題の ISR について
勉強不足で、ただ ISR を調べるつもりだったのに、すべてのレンダリング手法を振り返ることになってしまいました...
それではここから本題の ISR についてのお話をしていこうと思います。
SSG ではビルド時や初回リクエスト時のキャッシュを使い回すことで効率的に表示することが可能になっていますが、その反面コンテンツが更新された場合にキャッシュが邪魔をして差分が反映されるのに時間がかかりすぎる。という問題がありました。
その問題を解決したのが ISR で、 SSG の挙動に加えて設定した時間ごとにバックグラウンドでデータの再取得と再レンダリングを行い HTML を再生成を行います。
使用方法は getStaticProps で return するオブジェクトの中の revalidate という プロパティ に対して秒数を指定するだけで、非常に簡単です。
この revalidate で設定した秒数ごとに、バックグラウンドでデータの再取得と再レンダリングが行われる仕組みになっています。
ex) revalidate を 60 秒と設定した場合の流れ
- 1.初回リクエスト時はキャッシュを返す
- 2.60 秒経過した後にリクエストがあった場合コンテンツはキャッシュを表示しつつ、バックグラウンドでデータの再取得と再レンダリングを行う
- 3.2.の後にリクエストがあった場合には、2.の時にバックグラウンドで再生成されたキャッシュを返す(バックグラウンドの再生成が失敗した場合でも古いページは変更されない)
ISR は非常に設定が簡単で便利な仕組みですが、変更した内容が即座に反映されていてほしかったり、初回リクエストユーザーにも最新の状態を確認してほしい場合には、 SSR や CSR の方がよかったりするようなので、求められる仕様によって柔軟に対応する能力が求められそうです。
vercel にデプロイすると、実際に ISR が正しく実行されているかを確認できるようです。
ISR でキャッシュされたページの Response Headers に x-vercel-cache: HIT
が含まれているかを見ると判断可能。
ISR の場合、 getStaticProps で revalidate を設定するのと合わせて、SSG の話の時に出てきた getStaticPaths の fallback: 'blocking'
を設定しないと、revalidate 中のデータの再取得と再レンダリングがサーバー側で実行されないので注意が必要です。
ISR を利用する場合の懸念点として挙げられる"初回リクエスト時はキャッシュを返す"という点を解決する手法として、SWR を使ってクライアント側でマウント後に最新のデータをフェッチすることができるので、一瞬古いキャッシュが表示されるけど、その後に最新のコンテンツを表示することができるようです。
https://twitter.com/chibicode/status/1299500165418479616?s=20&t=3Cf1SXnfajafRXvfVi3-mA
参考
- https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration
- https://qiita.com/thesugar/items/47ec3d243d00ddd0b4ed
- https://zenn.dev/catnose99/articles/8bed46fb271e44
- https://zenn.dev/takuyakikuchi/articles/2f7e54bdafce52
- https://zenn.dev/a_da_chi/articles/d590a69e6991d2
- https://rabiloo.com/ja/blog/ssr-csr
- https://www.gaji.jp/blog/2022/08/08/10715/
- https://guydumais.digital/blog/next-js-the-ultimate-cheat-sheet-to-page-rendering/
- https://www.ragate.co.jp/blog/articles/10524
さいごに
今までは感覚的にその場しのぎで fallback とか revalidate とかの設定をしていた気がしますが、それぞれの特徴に応じて使い分ける必要があるということを改めて理解したので、仕様やユーザーが利用する場面を想像して使い分ける必要がありそうですね。
個人的には Jamstack なブログとかメディアとかを作ることが多いので、大体 ISR を使う場面が多くなりそうな気がしましたが、レンダリングの種類を理解しておくと応用が効きそうなので、知っておいて決して損はない知識だなと思いました。