Open12

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

zizi

Next.jsでブログっぽいページを作ろうと思ったのですが、SSR/ISR/SSGそれぞれどんな形で作るのがいいのか悩んだのでざっくり学んだことわかったことを書こうと思います。
今回の環境は以下です。

環境

  • Next.js : 13.2.3 (appDir 利用)
  • pnpm : 7.28.0
  • typescript : 4.9.5

Headless CMSとしてmicroCMS

※ 間違っていたらご指摘ください...

zizi

まず概要から

SSR (Server Side Rendering)

各リクエストに対してサーバーサイドでレンダリングを実行し返却する。

ISR (Incremental Static Regeneration)

リクエストに対してサーバーサイドでレンダリングを実行し返却する。ただし、レンダリング実行後指定された時間レンダリング結果をキャッシュし、指定された時間が過ぎた後にリクエストが来るまでキャッシュされた結果を返す。雰囲気的にはSSRとSSGのあいのこ。
現状、ホスティングできるインフラが制限される。

SSG (Static Site Generation)

ビルド時に全てのページを静的ページとしてレンダリング・ビルドする。
動的なデータが必要な場合は再ビルドが必要になる

zizi

ちょっと道がそれますが、Next.js 13から導入されたappディレクトリでは、インターフェースが大きく変更されて、初めに見た時はかなり面食らいました。

最初に触って驚いたことと言うと

  • React Server Componentがデフォルトになった
    useStateなどのお馴染みのクライアントサイドでのReact実行には、"use client"と明示的にClient Componentであることを宣言する必要がある
  • getStaticPropsgetStaticPathsgetServerSidePropsがなくなった
    これまで、SSRなどで使っていた上記予約命名関数たちはなくなって、generateStaticParams関数やfetch関数にキャッシュ戦略を与えるオプション方式に変更されました。挙動として大きく変わる部分はなかったと思いますが、割と書き振りに変化があったので驚きました。Next.js 13以前を知っている方ならmigration guideを読むとわかりやすいかも

https://beta.nextjs.org/docs/upgrade-guide#step-6-migrating-data-fetching-methods

他にも色々変化はありましたが、上記が割と最初に触ってなれなかったことでした。

zizi

閑話休題。

作りたいブログの形で色々タイプがあると思いますが、とりあえず今回作りたいのはオーソドックスに以下のようなルートがあるページとしました。

  • /posts
    投稿したブログの一覧があるページ。ページネーションとかはとりあえず考えてないです。
  • /posts/{slug}
    投稿したブログの記事ページ。slugがmicroCMSでいうcontentIDにあたる部分です。

まず、詳しい実装は後にして、それぞれのビルド・レンダリング方式でどのようなメリット・デメリットがあるか考えてみます。

まずは、SSRから

zizi

Server Side Rendering

一覧ページ・記事ページそれぞれ、リクエストがあったタイミングでデータを取得して、レンダリングして返却。

メリット

  • コンテンツの追加・削除時に、特に何もせずともリアルタイムで更新がかけられる。

デメリット

  • 各リクエストに対してレンダリングが発生するため、他2つに比べると若干速度が遅くなる可能性がある。

備考

レンダリング結果をCDNにキャッシュさせれば、SSGと同様の速度を出すことも可能だが、キャッシュ戦略を考える必要がある。もしくは、コンテンツ更新時にCDNに対してrevalidateをかければ、キャッシュの破棄タイミングはそこまで意識しなくてもいいかも。

zizi

Incremental Static Regeneration

一覧ページ・記事ページそれぞれ、リクエストがあったタイミングでレンダリング。その後、指定時間キャッシュさせ、指定時間が切れた次のリクエストまではキャッシュされた内容を返す。

メリット

  • SSGと同等の速度が出せる。
  • コード内で、キャッシュ戦略に近いものをかけるためわかりやすい

デメリット

  • ホスティングできるインフラが制限される
  • コンテンツ更新 〜 次のrevalidateの時間までは古いコンテンツが表示される可能性がある。

備考

デメリットの二つめに関しては、Next.jsのドキュメント内にon-demand revalidationというのがあり、コンテンツ更新時に強制的にrevalidateすることでリアルタイム性を担保できはする。(ただし、現状のNext 13.2.3では、appDir内で該当のAPIルートは作成できないため、これだけのためにpagesディレクトリも併用する必要がある)
https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration#on-demand-revalidation

zizi

Static Site Generation

ビルドのタイミングで一覧と記事詳細を取得して、静的ページとして吐き出し、それを静的配信する。

メリット

  • 既にレンダリングはビルドのタイミングで終わっているため速度が速い

デメリット

  • コンテンツの更新をする場合、再ビルドする必要がある

備考

コンテンツ数が多い場合は、全てのページを再ビルドするため長くなる可能性あり。

zizi

大枠で、メリット・デメリットを挙げたが、割と課題についてはインフラ周りなどでカバーできるので、本当の選定基準としては以下になるかと思います。

SSR

様々な要件に対して、無難。Node.jsが実行できる環境ならば、どこでも動くと思われる。
速度を出すためにキャッシュを使う場合のみ、追加でインフラの設定が必要なのでそこだけ検討する必要あり。

ISR

現状ホスティングできる環境が絞られすぎているため、そこの検討が必要あり。
しかも、現状フレームワーク側でのロックインがあるため、一応長期で動かすことを想定している場合は、仮に機能が丸ごとなくなった場合などのリスクヘッジを一応考えておくといいかも。

SSG

厳格なリアルタイム性が求められておらず、コンテンツ数が限られていて更新頻度が低い場合においては、かなり無難。
コンテンツの更新時に再ビルドをする処理だけ、github actionsか何かで設定できれば問題ない。

zizi

今回作るのは基本的なブログサイトで、コンテンツ数も少なく、更新頻度も別に高いわけじゃないので、SSGをメインでやっていこうと思います。ホスティングはmicroCMSがvercelの再ビルドさせる設定をデフォルトで持っていること、変にインフラを意識しない構成で楽なことからvercelでやります。

SSGで動かす場合の主な設定は、以下です。

  • 全てのフェッチ系をforce-cacheに変更する
  • appディレクトリとgenerateStaticParamsによって生成されたパス以外は404に直接飛ぶように変更する
zizi

全てのフェッチ系をforce-cacheにする

fetch関数のオプションに、すべてキャッシュする設定があるので、それを差し込みます。
ただ、デフォルトでforce-cacheらしいので、特にもともと設定をしていなければそのままでも大丈夫そうです。
https://beta.nextjs.org/docs/api-reference/fetch#optionscache

ここは若干自分もまだ理解しきれていないのですが、dynamicオプションを、各ページないしはレイアウトに配置することで同じような変更の仕方もできるっぽいです。
https://beta.nextjs.org/docs/api-reference/segment-config#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ディレクトリの新しいモデルは、getServerSidePropsgetStaticPropsのページディレクトリ単位での「ありかなし」というバイナリなモデルより、もっと詳細で粒度の高いフェッチ単位でのキャッシュコントロールを好みます。このdynamicオプションは、よりシンプルなマイグレーションのための過去のモデルに戻るための便宜上の方法です。
(訳:筆者)

とあり、あんまり使わない方がいいのかもです。

zizi

ちなみに、上記の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 }
})
zizi

appディレクトリとgenerateStaticParamsによって生成されたパス以外は404に直接飛ぶように設定する

SSGの場合は、staticで生成されているパス以外は404を返す必要があるので、その設定をします。
今回は、すべてのページで同じ設定でいいと思ったので、レイアウトページに設置しました。

export const dynamicParams = false

ここの値がtrueの場合は、生成されていないパスにリクエストを投げたとしても、直接404には行かなくなるので、ISR/SSR時に存在しない記事ページの場合の処理を入れる必要がありそうです。