イーストで案件に利用しているCMSである、SitecoreにはXM CloudというSaaS製品があります。
XM CloudはデフォルトでNext.jsを利用して開発を行うことになります。
そこで、Next.jsについて調査しました。

はじめに

Next.jsは、ReactベースのJavaScriptフレームワークです。
JavaSriptによってページのHTMLを生成するのですが、その生成方式がいくつかあります。
ページの特性に合わせて最適なものを選択することで、パフォーマンスやユーザー体験が向上するため、適切に使うことが重要です。

生成方式の種類と概要

  • Static Site Generation(SSG):サーバー側で事前にHTMLを生成しておく方式
  • Incremental Static Regeneration(ISR):SSGの発展系で、HTMLに期限を設けることで再生成することができる
  • Server-side Rendering(SSR):リクエストのたびにサーバー側でHTMLを生成する方式
  • Client-side Rendering(CSR):JavaSriptを送ってクライアント側でHTMLを生成する方式

Static Site Generation(SSG)

特徴(公式ドキュメント

サイトのビルド時にHTMLを生成し、アクセスされた時にそのHTMLを返す

ユーザーのアクセスより前にHTMLを生成するので、ページの表示速度は速くなります。
Next.jsとしても、表示の速度面からSSGを推奨しているとのことです。(参考:Next.jsのレンダリング
ただしページ数が多くなるにつれて、工夫しないとビルドに時間がかかるようになるので注意が必要です。

ビルド時に外部のデータ(DBのデータやCMSの記事等)を取得して生成ができる

getStaticProps関数を使用することで、ビルド時に外部のデータを取得してHTMLを生成することもできます。
getStaticProps関数に加えてgetStaticPaths関数を併用することで、ブログのような動的なURLに対応したHTMLを生成することもできます。
例えばファイル名を/blog/2024/[article].tsxとしてgetStaticPaths関数を利用すると、/blog/2024以下の全ての記事をCMS等から取得して静的HTMLとして生成する、というようなこともできます。
なお、ファイル名を[]で囲う必要があります(参考:動的ルーティング)。

開発時の注意点

開発環境ではSSGを使用していても静的HTMLは生成されないため、実際の表示速度の比較等はできません。

使うべきサイト

  • LPやドキュメントのような、ページの更新や追加がめったにないもの
  • ブログやECサイトのような、ページの更新や追加はあるもののリアルタイム性はないもの

コード例

以下のコード例は、公式ドキュメントを参考にしたものです。

export default function Page({ data }) {
  // ここで取得したデータをもとにページを表示したりする
  // 例:return <h1>{data.title}</h1>
}

//ここで取得するデータ用のパスを取得する
export async function getStaticPaths() {
  //この例では、APIにアクセスして、取得できたdatasのidを配列にして返している
  //このidをもとに、動的にHTMLを生成する
  const res = await fetch('https://.../datas/api')
  const datas = await res.json()
 
  const paths = datas.map((data) => ({
    params: { id: data.id },
  }))
 
  //取得したidをgetStaticProps関数で利用するために返す
  //getStaticPathsは動的なURLに対応するため、ビルド時に存在しないURLにアクセスされた時の動作もfallbackで指定する必要がある
  return { paths, fallback: false }
}
 
//ここでビルド時にAPI等からデータを取得する
export async function getStaticProps({ params }) {
  //ここでgetStaticPathsで取得したidをもとにapiにアクセスし、datasのデータを取得している
  const res = await fetch(`https://.../datas/api/${params.id}`)
  const data = await res.json()
 
  // 取得したデータをコンポーネントで使うために返す
  return { props: { data } }
}

解説

getStaticPaths関数のfallbackについて(参考:getStaticPathsの戻り値

動的ルーティングにしたページは、ビルド時に存在しないURLにアクセスされた時の動作を決める必要があります。
例えば、ビルド時に/blog/2024に属する記事が10個あったとします。
その後、11個目の記事を作成しました。
この時、/blog/2024/11にアクセスされた時の処理を、fallbackの値によって以下の3通りから選べます。

  1. fallback: falseにする
    この場合、404ステータスを返します。
    後からページを追加することが少ない場合に有効です。
  2. fallback: trueにする
    この場合、まずデータがない状態のページ(フォールバックバージョンと呼ばれます)をサーバーから返します。その後、バックグラウンドでデータを取得し、ページにデータを入れ込みます。
    また、一度アクセスされてHTMLが生成されたページはそのまま保持されるので、次回以降のアクセスは速くなります。
    HTMLにしたいデータが大量にある場合、fallback: trueにした上で一部のページのみビルドすることで、ビルドを高速にしつつSSGの利点も受けられます。
    なお、Googleなどのウェブクローラーやnext/linkまたはnext/routerによる遷移時は、後述のfallback: 'blocking'と同じ処理をします。
  3. fallback: 'blocking'にする
    この場合、サーバー側でデータを取得し、HTMLを生成してからそれを返します。
    ですので、フォールバックバージョンは返されません。
    処理としては後述のSSRと同様です。
    また、trueの時と同じく一度アクセスされてHTMLが生成されたページはそのまま保持されるので、次回以降のアクセスは速くなります。

Incremental Static Regeneration(ISR)

特徴(公式ドキュメント

静的に生成したHTMLを再生成できる

ISRは、SSGの発展形とも呼べるものです。SSGの特徴に加えて、生成したHTMLに期限を設けることでページのデータを更新することができます。
動作の流れは以下のようになります。

  1. ビルドをしてHTMLを生成します。これをページAとします。
    ここで、ページAには10秒の期限をつけたとします。
  2. ページAの生成後、10秒以内のアクセスであればサーバーはそのままページAを返します。
  3. 10秒経過以降にアクセスがあった場合は、以下の処理が行われます。
    1. 一旦ページAを返す
    2. バックグラウンドでデータを再取得しページA'を生成する
    3. ページAは破棄する
  4. これ以降のアクセスはページA'を返すようになります。
  5. 再度10秒経つと、同様に再生成されます。
    こうすることでSSGの利点を受けつつ、弱点だったデータの更新にも対応できます。

使うべきサイト

SSGを利用するサイトで、ページの更新や追加がある場合はISRにするとビルドの手間を省くことができます。

コード例

以下のコード例は、公式ドキュメントを参考にしたものです。

export default function Page({ datas }) {
  // ここで取得したデータをもとにページを表示したりする
  // 例:return <h1>{data.title}</h1>
}

//ここで取得するデータ用のパスを取得する
export async function getStaticPaths() {
  const res = await fetch('https://.../datas/api')
  const datas = await res.json()

  const paths = datas.map((data) => ({
    params: { id: data.id },
  }))

  return { paths, fallback: 'blocking' }
}
 
//ここでビルド時にAPI等からデータを取得する
export async function getStaticProps() {
  const res = await fetch(`https://.../datas/api/${params.id}`)
  const data = await res.json()
 
  return {
    props: {
      data,
    },
    // revalidateを設定するとISRになる
    revalidate: 10, // 単位は秒
  }
}

解説

getStaticProps関数のrevalidateについて

getStaticProps関数のreturnにrevalidateを追加することで、HTMLの再生成までの期限を設定できます。
SSGからISRにするには、この設定のみでできます。

Server-side Rendering(SSR)

特徴(公式ドキュメント

サーバーへのリクエスト時にHTMLを生成する

SSRにすると、ビルド時にはHTMLを生成せず、ユーザーがアクセスする度にサーバーで生成して返します。
そのため、SSGより表示速度は遅くなりますが、より新しいデータを表示できます。

ビルド時に外部のデータ(DBのデータやCMSの記事等)を取得して生成ができる

SSGのgetStaticProps関数と同様に、ビルド時に外部のデータを取得してHTMLを生成することができます。

使うべきサイト

  • ユーザーページなど、パーソナライズされているもの
  • 頻繁にデータが更新されるもの

コード例

以下のコード例は、公式ドキュメント(ここにURL)を参考にしたものです。

export default function Page({ data }) {
  // ここで取得したデータをもとにページを表示したりする
  // 例:return <h1>{data.title}</h1>
}
 
// アクセス時に毎回実行され、API等からデータを取得する
export async function getServerSideProps() {
  const res = await fetch(`https://.../data/api`)
  const data = await res.json()
 
  // データをページに渡す
  return { props: { data } }
}

解説

getServerSideProps関数について

この関数があることによって、ページの生成方式がSSRになります。
中身はgetServerSideProps関数とほぼ同じです。

Client-side Rendering (CSR)

特徴(公式ドキュメント

ブラウザ(クライアント側)でページを生成する

サーバーは最低限のHTMLとJavaScriptを送信し、ブラウザが受け取ったJavaScriptによってHTMLを生成します。
SPA(シングルページアプリケーション)にも用いられているReactの技術であり、ページの一部のみ更新することができます。
また、ローカルストレージなどのブラウザの機能を利用する場合にも使います。
なお、サーバー側は無関係なため、SSGやSSRと併用もできます。
利用にはReactのuseEffectを使用します。

利用時の注意点

ブラウザ上でJavaScriptによってページを生成するため、JavaScriptの実行を無効化していると表示できません。
また、検索エンジンのクローラーによってはJavaScriptを実行しないため、SEOが不利になる可能性があります。
そして、ブラウザ上でHTMLを生成するため、処理が重い場合はローディング画面等を表示する必要があります。

使うべきサイト

  • ブラウザの機能を利用したいもの
  • ページは遷移させたくないが、ユーザーの入力によって表示を切り替えたいもの

コード例

以下のコード例は、公式ドキュメントを参考にしたものです。

import React, { useState, useEffect } from 'react'
 
export function Page() {
  const [data, setData] = useState(null)
  //このページに遷移時、ここでデータを取得する
  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch('https://.../data')
      const result = await response.json()
      setData(result)
    }
  }, [])
 
  return <p>{data ? data : 'ロード中'}</p>
}

解説

useEffectについて

Reactに関する説明になってしまいますが、簡潔に記載します。
第一引数にデータを取得するための関数を入れ、第二引数には関数を実行するトリガーになる変数を入れます。
第二引数を空の配列にした場合、ページに遷移した時のみ非同期に実行されます。
そして実行が完了すると、取得したデータでページを再生成します。

終わりに

ユーザー体験の面でも、まずはISR(SSG)で実装できないかを考えるのが良さそうです。