💻

もう迷わないNext.jsのCSR/SSR/SSG/ISR

2021/03/25に公開

はじめに

Next.jsで一番最初の詰まりどころと言えば、「CSR/SSR/SSG/ISRとあるけどデータ取得はどのやり方でやれば良いか」という点ではないでしょうか。
自分の中でようやくこの辺りの整理ができたので、この記事ではCSR/SSR/SSG/ISRとは何ぞやというところからそれぞれの使い分けについて書いていこうと思います。

CSR/SSR/SSG/ISRとは

CSRとは

CSRはClient Side Renderingの略で、日本語に訳すとクライアント側でのレンダリングです。

CSRではクライアントのリクエストに対して空のHTMLとJSを返し、クライアント側でJSを実行してレンダリング、及びデータ取得を行います。
Reactのみを使ってSPAを作る場合にuseEffectの中でデータをfetchして結果をuseStateに渡して表示するというお馴染みのやり方です。
全てがクライアント側で完結するので、実装も運用もシンプルです。

しかしCSRには主に以下の2つの問題があります。

  • クライアントのリクエストに対して空のHTMLを返すため、SEOとOGPに対応できない
  • クライアント側でJSを実行するため、初期表示が遅い

この問題を解決するために生まれたのが後述するSSRです。

SSRとは

SSRはServer Side Renderingの略で、日本語に訳すとサーバ側でのレンダリングです。

SSRではクライアントのリクエストに対してサーバ側でデータを取得してHTMLを生成し、それを返します。
サーバ側でHTMLを生成するため、SEOとOGPに対応可能ですし、初期表示も高速です。

Next.jsではSSR用にgetServerSidePropsという関数が用意されているので、その中にデータを取得する処理を書きます。

export async function getServerSideProps(context) {
  return {
    props: {}, // will be passed to the page component as props
  }
}

さてこれにてめでたし、めでたし…とはならずに人類はさらなるパフォーマンスを追い求めます。

SSGとは

SSGはStatic Site Generationの略で、日本語に訳すと静的サイト生成です。

SSGではビルド時にサーバ側でデータを取得してHTMLを生成し、リクエストに対してそれを返します。
事前にHTMLを生成するため、CDNにキャッシュさせることができ、SSRよりパフォーマンスに優れます。

Next.jsではSSG用にgetStaticPropsという関数が用意されているので、その中にデータを取得する処理を書きます。

export async function getStaticProps(context) {
  return {
    props: {}, // will be passed to the page component as props
  }
}

さらにNext.jsではSSG用にgetStaticPathsという関数が用意されているので、その中にビルド時に生成するページのリストを指定します。
これはpages/blog/[id].tsxのようなDynamic Routesを使用している場合に有用です。

export async function getStaticPaths() {
  return {
    paths: [
      { params: { ... } }
    ],
    fallback: false
  };
}

上記のfallbackオプションにはビルド時に生成するページのリストに含まれていないページにアクセスがあった場合の挙動を設定します。
falseの場合は404ページを返すようになります。
これは例えば、/blog/1/blog/2をビルド時に生成するページのリストに指定している場合に/blog/3へのアクセスには404ページを返すということです。

ここまでのSSGの説明を読んで以下のような問題を思い浮かべる方がいらっしゃるかもしれません。

  1. ビルド時にサーバ側で取得するデータが動的な場合はどうすれば良いか
  2. ビルド後に/blog/3が追加される場合はどうすれば良いか

1つ目については素直にSSRにすることで、リクエストに対してサーバ側でデータを取得してHTMLを生成するようになるので解決します。
2つ目については使用しているCMSがWebhookに対応していれば、コンテンツが追加される度にWebhookで再ビルドするようにすれば解決します。

どちらの問題も一応今までの機能で解決できるものの、これらの問題をよりスマートに解決してくれるのが後述するISRです。

ISRとは

ISRはIncremental Static Regenerationの略で、日本語に訳すと定期的な静的再生成です。

ISRではビルド時にサーバ側でデータを取得してHTMLを生成した上で、stale-while-revalidateというキャッシュの仕組み[1]を使って指定時間経過後にリクエストがあった場合はサーバ側でデータを再取得してHTMLを再生成し、次のリクエストに対してそれを返します。
つまり初回はSSG、その後はSSRといった感じで両者のいいとこ取りができます。

Next.jsではSSG用のgetStaticPropsとgetStaticPathsにオプションを指定することでISRを使用できます。

// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// revalidation is enabled and a new request comes in
export async function getStaticProps() {
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  return {
    props: {
      posts,
    },
    // Next.js will attempt to re-generate the page:
    // - When a request comes in
    // - At most once every 10 seconds
    revalidate: 10, // In seconds
  }
}

// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// the path has not been generated.
export async function getStaticPaths() {
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  // Get the paths we want to pre-render based on posts
  const paths = posts.map((post) => ({
    params: { id: post.id },
  }))

  // We'll pre-render only these paths at build time.
  // { fallback: blocking } will server-render pages
  // on-demand if the path doesn't exist.
  return { paths, fallback: 'blocking' }
}

getStaticPropsのreturnでrevalidate: 秒数を指定すると、指定時間経過後にリクエストがあった場合はサーバ側でデータを再取得してHTMLを再生成し、次のリクエストに対してそれを返します。

またgetStaticPathsのreturnでfallback: true or 'blocking'を指定すると、ビルド時に生成するページのリストに含まれていないページにアクセスがあった場合はHTMLを生成してそれを返します。
この時、fallback: trueの場合は一旦データが空の状態のHTMLを返し、getStaticPropsを実行してサーバ側でデータを取得してから再レンダリングするような挙動(この時router.isFallbackがtrueになるのでこれを使用してローディング状態のハンドリングができます)になり、fallback: 'blocking'の場合はサーバ側でデータを取得してHTMLが生成されるまで待機するような挙動になります。

このようにISRを使用することで動的なデータを含むページでもSSGで対応することが可能になります。
ただし指定時間内のリクエストや指定時間経過後の最初のリクエストではキャッシュされたページを返すので、頻繁に変更され得るデータの場合に問題がないかの検討が必要です。

CSR/SSR/SSG/ISRの使い分け

まず最初に検討すべきはCSRで済むか済まないか(=クライアント側でデータを取得するか、サーバ側でデータを取得するか)です。

CSRとはで書いた通り、CSRには主に以下の2つの問題があります。

  • クライアントのリクエストに対して空のHTMLを返すため、SEOとOGPに対応できない
  • クライアント側でJSを実行するため、初期表示が遅い

これらの問題を受け入れられる場合はCSRで良いと思います。
CSRであれば全てがクライアント側で完結し、実装も運用もシンプルなので、CSRで済むのであればそれに越したことはないと個人的には考えています。
Next.jsを使用しているからといってSSRをしなければならないわけではない(Next.jsはもはやSSRだけではなく、高いDXとパフォーマンスの最適化など様々なメリットがある)ですし、サーバ側の機能を使わないということはサーバを建てる必要がなく、静的アセットとしてS3などにホスティングすることができるため、デプロイ先の選択肢が多くなる(=ポータビリティが高い)というメリットもあると思います。

https://nextjs.org/docs/advanced-features/static-html-export

またNext.js(というかVercel)ではuseSWRというライブラリを使用することが推奨[2]されています。
useSWRはクライアント側でのデータ取得を宣言的に書けるようにしてくれて、さらにクライアントキャッシュをよしなに管理してくれる優れものです。

上記の問題を受け入れられない場合は以下の順番で検討していくのが良いかと思います。

  1. SSG(ISRなし)
  2. ISR
  3. SSR

SSG(ISRなし)を使うケース

以下の条件を全て満たす場合はSSGが良いでしょう。

  • ページに動的なデータが含まれない

コーポレートサイトに表示する情報をCMSで管理しているような場合でも、滅多に情報が変更になることはないとはいえ、変更を反映する必要はあると思うので、ビルド時にサーバ側でデータを取得してHTMLを生成しておけばOKというケースは少ないのではないかと思います。
なのでサーバ側でデータを取得する場合はほとんど後述するISRを使うことになるでしょう。

ISRを使うケース

以下の条件を全て満たす場合はISRが良いでしょう。

  • ページに動的なデータが含まれる
  • 強整合性が求められない

サーバ側でデータを取得する場合はほとんどISRを使うことになるのではないかと思います。

ただしISRとはで書いた通り、指定時間内のリクエストや指定時間経過後の最初のリクエストではキャッシュされたページを返すので、頻繁に変更され得るデータの場合に問題がないかの検討が必要です。

ISRとSSR+stale-while-revalidate

以下の記事で言及されているように、stale-while-revalidate対応のCDNであればSSRでもISRと同じような挙動を実現することが可能なのですが、SSRとの差別化点としてはデータのプリフェッチが行われる点が挙げられます。
https://zenn.dev/catnose99/articles/0b601c1f62019b

プリフェッチについては以下の記事で言及されているように、revalidateの設定によっては短い間隔で大量のページがプリフェッチされてしまうので注意が必要です。
https://zenn.dev/takepepe/articles/nextjs-isr-prefetch

逆にgetStaticPathsでpathsに空配列を、fallbackにblockingを指定して、getStaticPropsでrevalidateを指定すればISRでもSSR+stale-while-revalidatと同じような挙動を実現することが可能です。
この場合はビルド時にサーバ側でデータを取得してHTMLを生成することはせず、リクエストに対してサーバ側でデータを取得してHTMLを生成し、その後はstale-while-revalidateに基づいてキャッシュされます。

これら2つの違いはデータのプリフェッチが行われるか否かなので、サーバへの負荷とパフォーマンスを天秤にかけて選択する形になりそうです。

SSRを使うケース

以下の条件を全て満たす場合はSSRが良いでしょう。

  • 強整合性が求められる
  • Vercel以外にホスティングしており、ISRに対応していない

SSRとはで書いた通り、クライアントのリクエストに対してサーバ側でデータを取得してHTMLを生成するので、常に最新のデータを返すことができます。

またISRを使うケースで書いた通り、stale-while-revalidate対応のCDNであればSSRでもISRと同じような挙動を実現することが可能なので、Vercel以外にホスティングしており、ISRに対応していない場合に使うことになると思います。

番外編: ISR+CSR

ISRでキャッシュされたページをCSRで最新の状態に更新する、といったやり方も存在するようです。

以下の記事で言及されているように、Zennではこのやり方を使用してISRの記事ページでも記事作成者の場合はCSRで最新の状態に更新することで、記事を編集したのに反映されていないように見えるのを防いでいるそうです。こんな使い方もあるんですね。
https://zenn.dev/catnose99/articles/8bed46fb271e44

おわりに

以上のようにCSR/SSR/SSG/ISRとは何ぞやというところからそれぞれの使い分けについてまとめてみました。
ある程度は整理できたかなと思いますが、もし間違っている箇所があればご指摘お願いします。

またNext.jsのデータ取得についてのドキュメントもぜひ確認してみてください。

それでは良いNext.jsライフを!

脚注
  1. https://web.dev/stale-while-revalidate/ ↩︎

  2. https://nextjs.org/docs/basic-features/data-fetching#swr ↩︎

Discussion