昨年、Vercelで25,000ページ以上の静的生成ページを備えたNext.jsサイトをリリースしました。製品ページ、ブログ記事、ロケーションランディングページ、動的カテゴリーフィルター -- すべてを網羅しています。Incremental Static Regeneration(ISR)の約束は魅力的です。静的サイトの速さとサーバー側レンダリングコンテンツの鮮度を兼ね備えることができます。正直なところ、大体はうまくいきます。しかし25,000ページを超えると、ISRは50ページのマーケティングサイトとは異なる動作をします。エッジケースがメインケースになります。コストは増加します。ドキュメントで理論的に思えたキャッシュ無効化の問題が、非常に現実的になります。

これは私たちが始める前にあればよかったと思う記事です。ここにあるすべてのことは本番環境での経験 -- 実際のメトリクス、実際の請求のサプライズ、私たちが下した(そして後悔した)実際のアーキテクチャー上の決定 -- に基づいています。

ISR at Scale: Running 25,000+ Pages with Incremental Static Regeneration on Vercel

目次

ISRが内部で実際に何をしているのか

スケール問題に入る前に、ISRが何をしているのかについて同じページにいることを確認しましょう。Next.jsページでrevalidate: 60を設定したとき、実際のフローは次のようになります:

  1. デプロイ後の最初のリクエスト: ページがビルド時に事前レンダリングされた場合、Vercelはエッジキャッシュから提供します。そうでない場合(fallback: 'blocking'を返したかApp RouterでdynamicParams: trueを使用した場合)、サーバー側でレンダリングし、結果をキャッシュして、提供します。

  2. 再検証ウィンドウ内の後続リクエスト: キャッシュから提供。高速。コンピュート無し。

  3. 再検証ウィンドウの有効期限が切れた後の最初のリクエスト: 古いページが即座に提供され(これが「stale-while-revalidate」部分です)、バックグラウンド再生成がトリガーされます。次の訪問者は新しいページを取得します。

これは概念的には単純です。しかし25,000ページでは、そのバックグラウンド再生成ステップは滝のようになります。

// App Router (Next.js 14/15)
export const revalidate = 60; // seconds

export async function generateStaticParams() {
  // At 25k pages, you probably don't want to return all of them here
  const topPages = await getTop500Pages();
  return topPages.map((page) => ({ slug: page.slug }));
}

export default async function ProductPage({ params }: { params: { slug: string } }) {
  const product = await getProduct(params.slug);
  return <ProductTemplate product={product} />;
}

Stale-While-Revalidateのトレードオフ

人々を混乱させる点: ISRは常に再生成をトリガーするリクエストに古いコンテンツを提供します。これはバグではなく機能です -- 訪問者は決してレンダリングを待つ必要がないということです。しかし、それはまたあなたのコンテンツは常に少なくとも1リクエスト遅れているということも意味します。25,000ページのサイトで一部のページが週に1回訪問される場合、再検証ウィンドウが経過した後、再生成をトリガーするために訪問した人がいないため、その「1リクエスト遅れ」は数日前のコンテンツを誰かが見ることを意味する可能性があります。

なぜ25,000ページがすべてを変えるのか

小規模では、ISRは基本的に魔法です。大規模では、3つのことが変わります:

ビルド時間がボトルネックになる

すべての25,000ページをビルド時に事前レンダリングしようとすると、人生の選択を疑わせるビルド時間を見ることになります。各ページはデータを取得し、ReactをHTMLにレンダリングし、静的アセットを生成する必要があります。ページあたり200ms(CMS APIにヒットしている場合は楽観的)でも、それは5,000秒 -- 83分以上です。Vercelのプロプランにはビルドタイムアウトが45分あります。エンタープライズはさらに多くを取得しますが、それでも計算クレジットを消費しています。

キャッシュ無効化が本当の問題になる

25,000ページでは、コンテンツが変更されたときに「すべてを再構築」することはできません。外科的な無効化が必要です。VercelのrevalidatePath()とrevalidateTag()APIが役立ちますが、スケール時には独自の癖があります。

バックグラウンド再生成の負荷スパイク

5,000ページすべてがrevalidate: 60を持ち、同時にトラフィックを取得することを想像してください。それは毎分5,000のサーバーレス関数の呼び出しがバックグラウンドで発生しています。あなたのCMS APIはそれに対応できる必要があります。

ISR at Scale: Running 25,000+ Pages with Incremental Static Regeneration on Vercel - architecture

ビルド戦略: 事前レンダリング対延期

これは大規模なISRサイトのための最も重要なアーキテクチャー上の決定です。これが私たちが使用するフレームワークです:

ページカテゴリー カウント(当社の場合) 戦略 理由
トラフィック量の多いページ(トップ500) 500 ビルド時に事前レンダリング これらはデプロイ後すぐにヒットされます。コールドスタートペナルティなし。
中程度のトラフィックページ 4,500 fallback: 'blocking'で延期 最初の訪問者は約300ms待機します。その後はキャッシュされます。許容可能です。
ロングテールページ 20,000 fallback: 'blocking'で延期 ほとんどはデプロイ後数時間/数日は訪問されません。事前レンダリングの価値なし。

重要な洞察: デプロイ後の最初の1時間で誰も訪問しないページを事前レンダリングしないでください。 ビルド分とお金を無駄にしています。

// generateStaticParams - only return your high-traffic pages
export async function generateStaticParams() {
  // We use analytics data to determine the top pages
  const topPages = await fetch('https://api.example.com/pages/top?limit=500', {
    headers: { Authorization: `Bearer ${process.env.CMS_TOKEN}` },
  }).then(r => r.json());

  return topPages.map((page: { slug: string }) => ({
    slug: page.slug,
  }));
}

このアプローチで、ビルドはタイムアウトから約8分で完了するまで短縮されました。これは大きな違いです。我々はNext.js開発作業のコンテキストで同様の最適化戦略について書きました -- 原則は広く適用されます。

`dynamicParams`設定は重要です

App Routerでは、dynamicParams = true(デフォルト)を設定すると、generateStaticParamsで返されないページはオンデマンドでレンダリングされてキャッシュされます。それをfalseに設定するとは、事前レンダリングされていないページに対して404が返されます。25,000ページサイトの場合、ほぼ確実にtrueが欲しいです。

export const dynamicParams = true; // generateStaticParamsにないページのオンデマンドレンダリングを許可

実際に機能する再検証パターン

時間ベースの再検証

最も単純なアプローチ。revalidateを秒数に設定します。しかしどの数字?

数ヶ月間の調整の後、我々が着陸したのはここです:

コンテンツタイプ 再検証期間 理由
製品価格 60秒 価格は頻繁に変わり、顧客は古い価格に気付く
製品説明 3600秒(1時間) ほとんど変わらない、時間的に重要でない
ブログ記事 86400秒(24時間) 公開後はほぼ変わらない
カテゴリー/リストページ 300秒(5分) 新しい製品が表示されるが、軽微な遅延は許容可能
ロケーションページ 86400秒(24時間) アドレス情報はほぼ変わらない

我々が早期に犯した間違い: すべてを60秒に設定しました。これはCMS(当社の場合Contentful)APIに再生成リクエストを大量に送信し、トラフィックスパイク時にレート制限に達しました。

オンデマンド再検証

これはほとんどのコンテンツ更新に対する優れたアプローチです。時間ベースの再検証でポーリングする代わりに、コンテンツが実際に変更されるときに再生成をトリガーします:

// app/api/revalidate/route.ts
import { revalidateTag, revalidatePath } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const secret = request.headers.get('x-revalidation-secret');
  
  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
  }

  const body = await request.json();
  
  // Tag-based revalidation -- this is the way
  if (body.tag) {
    revalidateTag(body.tag);
    return NextResponse.json({ revalidated: true, tag: body.tag });
  }

  // Path-based revalidation as fallback
  if (body.path) {
    revalidatePath(body.path);
    return NextResponse.json({ revalidated: true, path: body.path });
  }

  return NextResponse.json({ error: 'No tag or path provided' }, { status: 400 });
}

その後、CMSでウェブフックを設定して、コンテンツが公開されるたびにこのエンドポイントをヒットします。これを、より長い時間ベースの再検証(24時間など)とペアリングします。

スケール時のタグベース再検証

これがNext.js 14以降が大規模サイトで本当に輝く場所です。フェッチリクエストにタグを付けてタグで無効化できます:

async function getProduct(slug: string) {
  const res = await fetch(`https://api.cms.com/products/${slug}`, {
    next: { 
      tags: [`product-${slug}`, 'products', 'all-content'],
      revalidate: 86400 // 24 hour safety net
    },
  });
  return res.json();
}

現在、単一の製品が更新されたときにrevalidateTag('product-blue-widget')を呼び出し、そのページだけが再生成されます。一括価格更新を実行するときは、revalidateTag('products')を呼び出し、すべての製品ページは次回の訪問時に再生成されます。

落とし穴: 25,000個の製品ページがあるサイトでrevalidateTag('products')を呼び出しても、すぐにはすべて再生成されません。すべてを古いとしてマーク付けします。次回の訪問時に再生成されます。これは重要です -- トラフィックが低いページはこれが数日間実際に更新されない可能性があることを意味します。

Vercel固有の落とし穴と制限

2024年初期からVercelで実行してきました。ドキュメントが十分に強調していないものはここにあります:

ISRキャッシュストレージ

Vercelはエッジネットワークキャッシュ内のISRページを格納しています。2025年の時点で、Vercel Data Cacheにはいくつかの制限があります:

  • プロプラン: ISRキャッシュが含まれており寛容ですが、非常に多くの量でのキャッシュの読み取り/書き込みにはコストがあります
  • エンタープライズ: カスタム制限ですが、料金を支払っています
  • キャッシュエントリは永遠に続きません: revalidate: falseでも、Vercelは最近アクセスされていないキャッシュエントリを削除できます。プロプランでトラフィックがない30日後に、ページがキャッシュから消えるのを見てきました。

サーバーレス関数の期間

バックグラウンド再生成はサーバーレス関数として実行されます。Vercel Proでは、デフォルトのタイムアウトは60秒です(最大300秒を設定できます)。ページが再生成に時間がかかる場合 -- 例えば、CMSが遅い、または多くの画像処理をしているため -- 再生成は無音で失敗し、古いページが提供され続けます。

3つの異なるAPIからデータを取得するページでこれにぶつかりました。修正は、Next.jsアプリと最も遅いAPI間にキャッシング層(Upstash経由のRedis)を追加することでした。

同時再生成制限

Vercelはこれについてハード数を公開していませんが、1,000以上のISR再生成が同時にトリガーされたとき(例えば、広く使用されるタグでrevalidateTagを呼び出した後)、スロットリングを観察しました。再生成はキューに登録され、すべてが一度に処理されるのではなく、数分間にわたって処理されます。これを計画してください。

コールドスタート

しばらく訪問されていないページ(エッジキャッシュから削除された)は、次の訪問でコールドスタートを経験します。当社のベンチマークでは:

  • ウォームキャッシュヒット: 15-40ms TTFB
  • 古い再検証(キャッシュから提供): 15-40ms TTFB(古いため同じ)
  • コールド再生成(キャッシュなし、ブロッキング): 400-1200ms TTFB(API応答時間に応じて)

スケール時の実際の本番コスト

お金について話しましょう。ここで人々は驚きを受けます。

Vercel Pro($20/月ベース)での当社の25,000ページサイト(ISR付き):

コスト要素 月次 ノート
Vercel Proサブスクリプション $20 ベースプラン
サーバーレス関数実行 $180-$340 トラフィックによって異なります。ISR再生成は関数呼び出しとしてカウントされます。
エッジ帯域幅 $90-$150 25kページと画像は合計します
Vercel Data Cache $40-$80 ISR用のキャッシュの読み取り/書き込み
合計Vercel $330-$590/月 トラフィック月によります
Contentful(CMS) $489/月 チームプラン。ISR再生成からのAPI呼び出しで無料ティアを超えました。
Upstash Redis(キャッシング) $30/月 CMS API呼び出しを減らすために追加
合計 $849-$1,109/月 月に約2M pageviewsを提供するサイト用

これは高い? 従来のサーバーセットアップと比べると、競争力があります。CDN上の静的サイトと比べると、高いです。ISR再生成関数呼び出しは最大の変動コスト -- ページが再生成されるたびに、それはサーバーレス関数を1-5秒間実行しています。

コンテンツが頻繁に変わらないサイトではISRの利点がその利点を超える場合、Astroベースのアプローチを探索した顧客とも協力してきました。コンテンツがめったに変わらないサイト、Astroを使った完全な静的ビルドでホストすることができます。

本番環境でのISRの監視とデバッグ

ISRの失敗はデフォルトでは無音です。古いページが提供され続け、再生成が数日間失敗しているかもしれません。ここが当社の監視セットアップです:

カスタム再生成ロギング

// lib/with-regeneration-logging.ts
export async function fetchWithLogging(
  url: string,
  options: RequestInit & { next?: { tags?: string[]; revalidate?: number } }
) {
  const start = Date.now();
  try {
    const res = await fetch(url, options);
    const duration = Date.now() - start;
    
    // Log to your monitoring service
    if (duration > 5000) {
      console.warn(`[ISR] Slow fetch: ${url} took ${duration}ms`);
      // Send to Datadog/Sentry/etc.
    }
    
    return res;
  } catch (error) {
    console.error(`[ISR] Fetch failed: ${url}`, error);
    // This is critical -- if fetch fails, regeneration fails
    throw error;
  }
}

Vercelの組み込みツール

VercelのダッシュボードはISRキャッシュヒット率と再生成カウントを表示します。Analyticsタブで、以下を探します:

  • 関数ログのキャッシュステータス: HIT, MISS, STALE
  • サーバーレス関数メトリクスのISR再生成期間
  • ISRルートのエラー率

`x-vercel-cache`ヘッダー

Vercelからのすべての応答はこのヘッダーを含みます:

  • HIT -- エッジキャッシュから提供、新鮮
  • STALE -- エッジキャッシュから提供、バックグラウンドで再生成トリガー
  • MISS -- キャッシュにない、オンデマンドでレンダリング

100のランダムなページをチェックし、10%以上がMISSを返すと警告するシンプルなモニターを設定しました -- それはキャッシュ削除の問題を示します。

アーキテクチャー上の決定: ISR対代替案

このスケールで1年以上ISRを実行した後、いつ使用するか、いつ使用しないかについての正直な見解です:

ISRを使用する場合:

  • 異なる頻度で変更される5,000-100,000ページがあります
  • コンテンツの鮮度は分単位で測定されます(秒単位ではなく)許容可能です
  • あなたはNext.jsにすでにコミットしています
  • あなたのチームはキャッシュ無効化を理解しています(スケール時には必須知識です)

代替案を検討する場合:

  • リアルタイムコンテンツが必要です(代わりにSSRまたはクライアント側フェッチを使用します)
  • あなたのサイトはほとんど変わりません(完全な静的ビルドは単純で安いです)
  • 500,000ページ以上があります(ISRは非常に多くのページカウントでストレインし始めます -- 分散ビルドアプローチを検討します)
  • コストが主な関心事です(自己ホストNext.jsとあなた自身のCDNは60-70%安くなります)

複雑なコンテンツアーキテクチャを持つクライアントの場合、ヘッドレスCMSセットアップを推奨することが多く、コンテンツタイプに応じてISR、SSR、完全な静的の間で切り替える柔軟性を提供します。

我々が実際に使用するハイブリッドアプローチ

当社の25KページサイトのすべてでISRを使用しません。内訳は次のようになります:

  • ISR: 製品ページ、カテゴリーページ、ロケーションページ(22,000ページ)
  • SSR: 検索結果、ユーザーダッシュボード、カート
  • 静的: About、Contact、Legal(ビルド時に生成、再検証なし)
  • クライアント側: リアルタイム在庫数、ユーザー固有の価格

このハイブリッドアプローチにより、「すべてISR」戦略と比較してサーバーレス関数コストを約40%削減しました。

当社のデプロイメントからのパフォーマンスベンチマーク

当社の本番展開からの実際の数字、2025年第1四半期にわたって測定:

メトリック ISRキャッシュヒット ISRキャッシュミス(ブロッキング) フルSSR(キャッシュなし)
TTFB(p50) 22ms 480ms 620ms
TTFB(p95) 58ms 1,100ms 1,450ms
TTFB(p99) 120ms 2,800ms 3,200ms
LCP(p50) 1.1s 1.8s 2.2s
CLS 0.02 0.02 0.05
Core Web Vitals合格率 96% 78% 64%

キャッシュヒットとミスの違いは劇的です。これは事前レンダリング戦略がそれほど重要である理由です -- トラフィックが多いページは常にウォームであることが必要です。

興味深い発見: revalidate: 60からrevalidate: 3600に低変更コンテンツに移動したとき、Core Web Vitals スコアが12%向上しました。再生成が少なくなったことは、より一貫性のあるキャッシュヒットを意味し、より一貫性のあるパフォーマンスを意味しました。

FAQ

ISRがVercelのパフォーマンスを低下させる前に処理できるページ数はいくつですか?

当社は25,000ページを重大な問題なく実行してきており、100,000ページ以上で動作しているデプロイメントを聞いたことがあります。ボトルネックはキャッシュのページ数ではなく、同時再生成の速度です。50,000ページすべてがrevalidate: 60を持っている場合、問題が発生します。コンテンツの変更頻度とトラフィックに基づいて再検証期間を広げます。

ISRはVercelでSSRより費用がかかりますか?

一般に、ISRは同じトラフィック量のSSRより大幅に安いです。ISRでは、ほとんどのリクエストはエッジキャッシュから提供されます(基本的に無料のコンピュート)。SSRでは、すべてのリクエストがサーバーレス関数を実行します。月に2M pageviewsのサイトの場合、ISRの関数呼び出し(再生成から)はフルSSRのおおよそ15%でした。

ISR再生成が失敗するとどうなりますか?

古いバージョンが提供され続けます。これは機能とリスクの両方です。ユーザーはエラーを見ませんが、古いコンテンツを見ている可能性があります。CMS APIの停止が6時間前のコンテンツを提供するページを意味する状況を経験してきました。監視を設定します。

App RouterでNext.jsでISRを使用できますか?

はい、実際にはApp Routerの方がクリーンです。ページレベルまたはレイアウトレベルでexport const revalidate = 60を使用し、フェッチ呼び出しでnext: { revalidate, tags }を使用します。generateStaticParams関数はgetStaticPathsを置き換えます。この記事で説明したすべてのことはPages RouterとApp Routerの両方で機能します、ただし2025年の新しいプロジェクトにはApp Router構文をお勧めします。

ISRを動的クエリパラメーターで処理するにはどうすればよいですか?

ISRはURLパスのみをキャッシュし、クエリパラメーターはキャッシュしません。?color=red?color=blueに異なるキャッシュ済みバージョンが必要な場合、実際のパスセグメント(/product/widget?color=redの代わりに/product/widget/red)を使用するか、バリエーションをクライアント側で処理する必要があります。これはフィルタリング実装で当社を驚かせました。

オンデマンド再検証はスケール時に信頼できますか?

ほぼ。revalidateTag()を呼び出してからキャッシュが実際にすべてのエッジロケーション間で無効化されるまで、10-30秒の遅延が時々あります。99%のユースケースでは、これは問題ありません。即座にグローバル無効化が必要な場合、キャッシュ破棄クエリパラメーターを追加するか、それらの特定のページに対してSSRを使用する必要があるかもしれません。

Vercelの代わりにNext.jsを自己ホストすべきですか大規模なISRサイトの場合?

チームによって異なります。自己ホスティング(例えばAWS上)はキャッシング動作に対して更に多くの制御を与え、スケール時に50-70%安いことができます。しかしあなたはCDNキャッシュ無効化の設定、ビルドパイプラインの管理、エッジ配布を自分で処理する責任があります。当社はVercelが提供するもの複製するのに数ヶ月かかったチームを見てきました。オプションを探索したい場合は、お問い合わせください -- 当社は両方を行ってきました。

25,000ページ以上のISRサイトに最適なCMSは何ですか?

当社はこのスケールでContentful、Sanity、Hygraphを使用してきました。Contentfulはウェブフックベースの再検証をよく処理しますが、レート制限は問題になります(キャッシングを計画します)。SanityのGROQLサブスクリプションはコンテンツ変更のリアルタイム認識に優れています。Hygraphのウェブフックシステムは堅牢です。主な要件は信頼性の高いウェブフック配信とバースト再生成トラフィックから処理できるAPIです。コンテンツモデルに基づいた詳細な推奨については、ヘッドレスCMS開発機能をご確認ください。