# ISR

Incremental Static Regeneration(ISR)とは、SSGをページ単位で動的に実行できるレンダリング方法です。

SSGのデメリットで見たように、SSGでは1つのコンテンツを変更するたびに全てのページを再ビルドする必要があります。 変更のないページも再ビルドしなければならないため、コンテンツの量が多ければ多いほど、ビルドとデプロイのコストが高くなってしまいます。 そのため、大規模な動的コンテンツを保有するサイトの場合、SSGでは対応が難しくなるでしょう。

ISRは、このようなSSGの問題を解決することができます。 ISRはページごとにキャッシュを保持し、ある一定の期間が過ぎたらバッググランドで再ビルドを実行して新しいコンテンツを生成します。 この間、ユーザーは既存のページを閲覧できるので再ビルドで待たされることはありません。

また、全てのページが再ビルドされるわけではなく、対象のページのみが再ビルドされます。 そして、新しいコンテンツはキャッシュされ、次のリクエストがあったタイミングで再利用されます。 ユーザーが次にアクセスしたときには、待たされることなく新しいコンテンツを閲覧することができます。

このようなプロセスをページ単位で実行することよって、動的にかつ効率よく配信することができます。

# ISRの仕組み

では、ISRの仕組みをNext.jsを例に見てみましょう。 ECサイトのようなWebページを想定してみましょう。

ISRがクライアントへ配信されるまでのステップは次のようになります。

# クライアントへ配信されるまでのステップ

ビルド時に実行

  1. アプリケーションをビルドする (next build)
  2. ビルド時にHTMLファイルが生成される

サーバ側(CDN)で実行

  1. ブラウザがサーバへリクエストをする
  2. サーバはリクエストを受け取り、生成されたコンテンツ(HTML)をブラウザ(クライアント)に返す

クライアント側で実行

  1. ブラウザ側でHTMLファイル解析し、JavaScriptファイルをダウンロードする
  2. JavaScriptファイルを実行しReactを起動する
  3. すでに生成されたコンテンツ(HTML)に対して、ハイドレーションを実行する
  4. ハイドレーション後、アプリケーションが機能する状態になる

ここまでは、SSGの仕組みと同じです。

では、ISRが再ビルドするまでのステップを見てみましょう。

# 再ビルドするまでのステップ

ISRは、ある一定の期間が過ぎたらバッググランドで再ビルドを実行して新しいコンテンツを生成します。 ここでは、10秒間で再ビルドが実行される想定で見てみましょう。

クライアント側

  1. ユーザーが商品一覧ページへアクセスする。このときはビルド済みの既存のページが表示される。
  2. 10秒が経過した後に、再度ページへアクセスする。このときはビルド済みの既存のページが表示される。
  3. バッググランドで再ビルドのリクエストが発生する。

サーバ側(Serverless Function)

  1. 対象のページのビルドを実行してキャッシュを更新する。

クライアント側

  1. 商品一覧ページへ再度アクセスすると、新しいコンテンツが表示される。

はじめに、Next.jsのISRでは再ビルドするまでの期間を設定します。

例えば、以下のような商品一覧ページがあるとしましょう。

// pages/products/index.tsx

export async function getStaticProps() {
  return {
    props: {
      // 外部APIへ非同期でデータを取得する
      products: await getProductsFromDatabase(),
    }
  }
}

// APIから取得したデータを元にコンテンツを生成する
export default function Products({ products }: Props) {
  return (
    <>
      <h1>商品一覧</h1>
      <ul>
        {products.map((product) => (
          <li key={product.id}>{product.name}</li>
        ))}
      </ul>
    </>
  )
}

このページで再ビルドするまでの時間を10秒間に設定します。

Next.jsではrevalidateオプションを使用します。






 




export async function getStaticProps() {
  return {
    props: {
      // 外部APIへ非同期でデータを取得する
      products: await getProductsFromDatabase(),
      revalidate: 10,
    }
  }
}

この設定で、最初のアクセスから10秒後に再ビルドのリクエストが発生します。

再ビルドのリクエストが発生すると、Next.jsが展開しているServerless Functionsというサーバ上でビルドが実行されます。 ビルドが成功すると、新しいコンテンツのキャッシュデータがNext.jsのエッジサーバ上に反映されます。 エッジサーバは世界中に点在しており、ユーザーに一番近いサーバでリクエストを受け付けることができます。 そのため、ユーザーがページへアクセスしたときに最新のデータを即座に配信することができます。

では、この設定をもとにISRのステップを見てみましょう。

まず、ユーザーが商品一覧ページへアクセスします。

ISR-仕組み-1

このときはビルド済みの既存のページが表示されます。

10秒後に、再度アクセスしてみましょう。

ISR-仕組み-2

このときもまだ、ビルド済みの既存のページが表示されます。 なぜなら、このタイミングで、再ビルドのリクエストが発生するため、表示できるコンテンツは既存のものしかないからです。

そして、バッググランドで再ビルドが発生します。

ISR-仕組み-3

ビルドが終わると、エッジサーバのキャッシュが新しいコンテンツに更新されます。

ISR-仕組み-4

そして、次に新しいユーザーがアクセスしたときに最新のコンテンツが表示されます。

ISR-仕組み-5

# 任意のタイミングで再生成する (On-Demand Revalidation)

前述した例ですと、アクセスがあった10秒後に再ビルドが実行されていました。

しかし、任意のタイミングで再ビルドしたいときもあると思います。 例えば、商品の価格変更をしたときや緊急時のアナウンスの通知など、アプリケーションの特性で要件が異なるでしょう。

Next.js 12.1からOn-Demand Revalidationという機能が追加されました。 これは任意のタイミングで再ビルドとキャッシュの更新ができる仕組みです。

例えば、次のようなAPIを定義します。

// pages/api/revalidate.js

export default async function handler(req, res) {
  if (req.query.secret !== process.env.MY_SECRET_TOKEN) {
    return res.status(401).json({ message: 'Invalid token' })
  }

  try {
    await res.revalidate('/products')
    return res.json({ revalidated: true })
  } catch (err) {
    return res.status(500).send('Error revalidating')
  }
}

このAPIエンドポイントに対して、クライアントや外部から次のようなリクエストを実行すると再ビルドを実行することができます。

https://<your-site.com>/api/revalidate?secret=<token>

revalidateオプションで時間を設定するよりも、任意のタイミングで最新コンテンツを生成することができるため、より柔軟性のある配信をすることが可能となります。

# メリット

# ビルド・デプロイパフォーマンスの向上

SSGが全てのページをビルドするのに対し、ISRはページ単位でビルドすることができます。 また、アプリケーションのビルド時に事前に生成するコンテンツの数を調整することができます。

例えば、getStaticPathsで事前にビルドする分だけデータを取得して、残りはISR実行時に生成するようにします。

export async function getStaticPaths() {
  // 事前に100商品だけビルドしておく。残りの商品はリクエストが発生した時にビルドする
  const products = await getProductsFromDatabase();

  const paths = products.map((product) => ({
     params: { id: product.id }
  }));

  return { paths, fallback: true };
}

このようにビルド・デプロイのコストを下げることで、リリースまでのリードタイムが短くなります。

# デメリット

# 最新のコンテンツではないときがある

ISRは指定の時間、もしくは任意のタイミングで再ビルドをバッググランドで実行します。 その間、ユーザーが見ているコンテンツは古いままになってしまいます。 サービスの特性上、そのあたりを許容できるか、できないかを見極める必要があるでしょう。

# ISRに対応したフレームワークが必要

ISRを実装するには、Next.js + Vercelなどのサービスが必要となります。 自前で実装するには、同じようなキャッシュの機構とエッジサーバの仕組みが必要になります。 CloudflareなどのCDNやエッジワーカーを使えば、同じような構成を作ることができますが、考慮する点も多くなるでしょう。 自前の場合は、実装コストとメンテナンスコストを踏まえつつ、適切なアーキテクチャを選択する必要があります。

# ISRに適したサービス

ISRは、SEO対策が必須で動的コンテンツに対応したいサービスに適しています。 SSGでは更新性が低く静的コンテンツが多いコーポレイトサイトなどに適していますが、ISRでは大規模な動的コンテンツにも対応できます。 例えば、パフォーマンス性と更新性が求められるECサイトや記事投稿サイトなどが当てはまるでしょう。 しかし、ユーザーのリクエストによってコンテンツを変える要件がある場合や、リアルタイム性が求められる場合はSSRがより適しているでしょう。