Next.jsでブログを作る時のSSR/ISR/SSGのそれぞれを軽くまとめてみる

Next.jsでブログっぽいページを作ろうと思ったのですが、SSR/ISR/SSGそれぞれどんな形で作るのがいいのか悩んだのでざっくり学んだことわかったことを書こうと思います。
今回の環境は以下です。
環境
- Next.js : 13.2.3 (appDir 利用)
- pnpm : 7.28.0
- typescript : 4.9.5
Headless CMSとしてmicroCMS
※ 間違っていたらご指摘ください...

まず概要から
SSR (Server Side Rendering)
各リクエストに対してサーバーサイドでレンダリングを実行し返却する。
ISR (Incremental Static Regeneration)
リクエストに対してサーバーサイドでレンダリングを実行し返却する。ただし、レンダリング実行後指定された時間レンダリング結果をキャッシュし、指定された時間が過ぎた後にリクエストが来るまでキャッシュされた結果を返す。雰囲気的にはSSRとSSGのあいのこ。
現状、ホスティングできるインフラが制限される。
SSG (Static Site Generation)
ビルド時に全てのページを静的ページとしてレンダリング・ビルドする。
動的なデータが必要な場合は再ビルドが必要になる

ちょっと道がそれますが、Next.js 13から導入されたappディレクトリでは、インターフェースが大きく変更されて、初めに見た時はかなり面食らいました。
最初に触って驚いたことと言うと
-
React Server Componentがデフォルトになった
useState
などのお馴染みのクライアントサイドでのReact実行には、"use client"
と明示的にClient Componentであることを宣言する必要がある -
getStaticProps
やgetStaticPaths
、getServerSideProps
がなくなった
これまで、SSRなどで使っていた上記予約命名関数たちはなくなって、generateStaticParams
関数やfetch関数にキャッシュ戦略を与えるオプション方式に変更されました。挙動として大きく変わる部分はなかったと思いますが、割と書き振りに変化があったので驚きました。Next.js 13以前を知っている方ならmigration guideを読むとわかりやすいかも
他にも色々変化はありましたが、上記が割と最初に触ってなれなかったことでした。

閑話休題。
作りたいブログの形で色々タイプがあると思いますが、とりあえず今回作りたいのはオーソドックスに以下のようなルートがあるページとしました。
-
/posts
投稿したブログの一覧があるページ。ページネーションとかはとりあえず考えてないです。 -
/posts/{slug}
投稿したブログの記事ページ。slugがmicroCMSでいうcontentIDにあたる部分です。
まず、詳しい実装は後にして、それぞれのビルド・レンダリング方式でどのようなメリット・デメリットがあるか考えてみます。
まずは、SSRから

Server Side Rendering
一覧ページ・記事ページそれぞれ、リクエストがあったタイミングでデータを取得して、レンダリングして返却。
メリット
- コンテンツの追加・削除時に、特に何もせずともリアルタイムで更新がかけられる。
デメリット
- 各リクエストに対してレンダリングが発生するため、他2つに比べると若干速度が遅くなる可能性がある。
備考
レンダリング結果をCDNにキャッシュさせれば、SSGと同様の速度を出すことも可能だが、キャッシュ戦略を考える必要がある。もしくは、コンテンツ更新時にCDNに対してrevalidateをかければ、キャッシュの破棄タイミングはそこまで意識しなくてもいいかも。

Incremental Static Regeneration
一覧ページ・記事ページそれぞれ、リクエストがあったタイミングでレンダリング。その後、指定時間キャッシュさせ、指定時間が切れた次のリクエストまではキャッシュされた内容を返す。
メリット
- SSGと同等の速度が出せる。
- コード内で、キャッシュ戦略に近いものをかけるためわかりやすい
デメリット
- ホスティングできるインフラが制限される
- コンテンツ更新 〜 次のrevalidateの時間までは古いコンテンツが表示される可能性がある。
備考
デメリットの二つめに関しては、Next.jsのドキュメント内にon-demand revalidationというのがあり、コンテンツ更新時に強制的にrevalidateすることでリアルタイム性を担保できはする。(ただし、現状のNext 13.2.3では、appDir内で該当のAPIルートは作成できないため、これだけのためにpagesディレクトリも併用する必要がある)

Static Site Generation
ビルドのタイミングで一覧と記事詳細を取得して、静的ページとして吐き出し、それを静的配信する。
メリット
- 既にレンダリングはビルドのタイミングで終わっているため速度が速い
デメリット
- コンテンツの更新をする場合、再ビルドする必要がある
備考
コンテンツ数が多い場合は、全てのページを再ビルドするため長くなる可能性あり。

大枠で、メリット・デメリットを挙げたが、割と課題についてはインフラ周りなどでカバーできるので、本当の選定基準としては以下になるかと思います。
SSR
様々な要件に対して、無難。Node.jsが実行できる環境ならば、どこでも動くと思われる。
速度を出すためにキャッシュを使う場合のみ、追加でインフラの設定が必要なのでそこだけ検討する必要あり。
ISR
現状ホスティングできる環境が絞られすぎているため、そこの検討が必要あり。
しかも、現状フレームワーク側でのロックインがあるため、一応長期で動かすことを想定している場合は、仮に機能が丸ごとなくなった場合などのリスクヘッジを一応考えておくといいかも。
SSG
厳格なリアルタイム性が求められておらず、コンテンツ数が限られていて更新頻度が低い場合においては、かなり無難。
コンテンツの更新時に再ビルドをする処理だけ、github actionsか何かで設定できれば問題ない。

今回作るのは基本的なブログサイトで、コンテンツ数も少なく、更新頻度も別に高いわけじゃないので、SSGをメインでやっていこうと思います。ホスティングはmicroCMSがvercelの再ビルドさせる設定をデフォルトで持っていること、変にインフラを意識しない構成で楽なことからvercelでやります。
SSGで動かす場合の主な設定は、以下です。
- 全てのフェッチ系を
force-cache
に変更する - appディレクトリと
generateStaticParams
によって生成されたパス以外は404に直接飛ぶように変更する

force-cache
にする
全てのフェッチ系をfetch関数のオプションに、すべてキャッシュする設定があるので、それを差し込みます。
ただ、デフォルトでforce-cache
らしいので、特にもともと設定をしていなければそのままでも大丈夫そうです。
ここは若干自分もまだ理解しきれていないのですが、dynamic
オプションを、各ページないしはレイアウトに配置することで同じような変更の仕方もできるっぽいです。
静的ページの場合はおそらく
export const dynamic = "error"
ですね。
ただし、migration noteのところに
The new model in the app directory favors granular caching control at the fetch request level over the binary all-or-nothing model of getServerSideProps and getStaticProps at the page-level in the pages directory. The dynamic option is a way to opt back in to the previous model as a convenience and provides a simpler migration path.
appディレクトリの新しいモデルは、
getServerSideProps
とgetStaticProps
のページディレクトリ単位での「ありかなし」というバイナリなモデルより、もっと詳細で粒度の高いフェッチ単位でのキャッシュコントロールを好みます。このdynamic
オプションは、よりシンプルなマイグレーションのための過去のモデルに戻るための便宜上の方法です。
(訳:筆者)
とあり、あんまり使わない方がいいのかもです。

ちなみに、上記のfetchのオプションを以下のようにすると、それぞれ、ISR・SSRになるそうです。
// SSR
// revalidateを0とcacheを'no-store'にして、キャッシュさせない
// fetch
fetch(..., {
cache: 'no-store',
next: { revalidate: 0 }
})
// ISR
// revalidateを60にして、60秒ごとにrevalidateする設定。
//(cache: "force-cache"はデフォルト)
fetch(..., {
next: { revalidate: 60 }
})

generateStaticParams
によって生成されたパス以外は404に直接飛ぶように設定する
appディレクトリとSSGの場合は、staticで生成されているパス以外は404を返す必要があるので、その設定をします。
今回は、すべてのページで同じ設定でいいと思ったので、レイアウトページに設置しました。
export const dynamicParams = false
ここの値がtrue
の場合は、生成されていないパスにリクエストを投げたとしても、直接404には行かなくなるので、ISR/SSR時に存在しない記事ページの場合の処理を入れる必要がありそうです。