ISR 大規模運行:在 Vercel 上運行 25,000+ 頁面的增量靜態再生成

去年我們在 Vercel 上發佈了一個包含超過 25,000 個靜態生成頁面的 Next.js 網站。產品頁面、部落格文章、位置登陸頁面、動態類別過濾器 -- 應有盡有。增量靜態再生成 (ISR) 的承諾很吸引人:以靜態網站的速度與伺服器渲染內容的新鮮度相結合。說實話嗎?它大多能交付。但在 25,000+ 頁面的規模下,ISR 的表現與你 50 頁行銷網站上的表現完全不同。邊緣情況變成了主要情況。成本不斷上升。文檔中似乎是理論性的快取失效問題變得非常真實。

這是我希望在開始前就存在的文章。這裡的一切都來自生產經驗 -- 真實的指標、真實的帳單驚喜,以及我們做出的真實架構決策(有時還後悔了)。

ISR 大規模運行:在 Vercel 上運行 25,000+ 頁面的增量靜態再生成

目錄

ISR 在幕後實際做什麼

在我們深入探討規模問題之前,讓我們確保我們在同一頁面上了解 ISR 是什麼。當你在 Next.js 頁面中設定 revalidate: 60 時,這是實際的流程:

  1. 部署後的第一個請求:如果該頁面在構建時預先呈現,Vercel 會從邊緣快取提供該頁面。如果不是(你返回了 fallback: 'blocking' 或在 App Router 中使用了 dynamicParams: true),它會進行伺服器端渲染,快取結果,然後提供該頁面。

  2. 再驗證窗口內的後續請求:從快取提供。快速。無計算。

  3. 再驗證窗口過期後的第一個請求:過期頁面立即提供(這是「過期重新驗證」部分),並觸發背景再生成。下一個訪問者會獲得新鮮頁面。

這在概念上很簡單。但在 25,000 個頁面的規模下,背景再生成步驟變成了消防栓。

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

export async function generateStaticParams() {
  // 在 25k 頁面上,你可能不想在這裡返回所有頁面
  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} />;
}

過期重新驗證權衡

讓人們困惑的是:ISR 始終為觸發再生成的請求提供過期內容。這是一個功能,而不是 bug -- 這意味著沒有訪問者需要等待呈現。但這也意味著你的內容總是至少落後一個請求。對於一個 25,000 頁面的網站,其中一些頁面每週只被訪問一次,在再驗證窗口後過期的「落後一個請求」可能意味著某人看到的內容是數天前的,因為沒有人訪問過來觸發再生成。

為什麼 25,000 個頁面改變了一切

在小規模下,ISR 基本上是魔法。在大規模下,三件事改變了:

構建時間成為瓶頸

如果你嘗試在構建時預先呈現所有 25,000 個頁面,你會面臨使你質疑人生選擇的構建時間。每個頁面都需要擷取其數據、將 React 呈現為 HTML,並生成靜態資產。即使每頁 200 毫秒(如果你擊中 CMS API 是樂觀的),那也是 5,000 秒 -- 超過 83 分鐘。Vercel 的 Pro 計畫的構建超時為 45 分鐘。企業版會獲得更多,但你仍在燃燒計算積分。

快取失效成為真正的問題

有 25,000 個頁面,你不能只在內容變更時「重建所有內容」。你需要精確失效。Vercel 的 revalidatePath()revalidateTag() API 有幫助,但在規模上它們有自己的怪癖,我們將涵蓋這些。

背景再生成負載峰值

想像 5,000 個頁面都有 revalidate: 60,它們同時都有流量。那是每分鐘發生的 5,000 個無伺服器函數呼叫。你的 CMS API 最好能夠處理這個。

ISR 大規模運行:在 Vercel 上運行 25,000+ 頁面的增量靜態再生成 - 架構

構建策略:預渲染什麼 vs. 延遲什麼

這是大型 ISR 網站最重要的架構決策。這是我們使用的框架:

頁面類別 計數(我們的情況) 策略 原因
高流量頁面(前 500) 500 在構建時預渲染 這些在部署後立即被點擊。無冷啟動罰則。
中等流量頁面 4,500 使用 fallback: 'blocking' 延遲 第一個訪問者等待 ~300ms,然後被快取。可接受。
長尾頁面 20,000 使用 fallback: 'blocking' 延遲 大多數在部署後數小時/數天內不會被訪問。不值得預渲染。

關鍵見解:不要預渲染部署後第一小時內沒有人會訪問的頁面。 你在浪費構建分鐘數和金錢。

// generateStaticParams - 只返回你的高流量頁面
export async function generateStaticParams() {
  // 我們使用分析數據來確定頂級頁面
  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();
  
  // 基於標籤的再驗證 -- 這是正確的方式
  if (body.tag) {
    revalidateTag(body.tag);
    return NextResponse.json({ revalidated: true, tag: body.tag });
  }

  // 基於路徑的再驗證作為備用
  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 中設定 webhook,以便在發佈內容時擊中此端點。我們將其與更長的基於時間的再驗證(例如 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 小時安全網
    },
  });
  return res.json();
}

現在當單一產品被更新時,你呼叫 revalidateTag('product-blue-widget'),只有那個頁面再生成。當你進行批量價格更新時,呼叫 revalidateTag('products'),所有產品頁面在其下一次訪問時再生成。

陷阱:在有 25,000 個產品頁面的網站上呼叫 revalidateTag('products') 不會立即再生成所有頁面。它將它們全部標記為過期。它們在下一次訪問時再生成。這很重要 -- 這意味著一些頁面如果流量很少,可能在數天內都不會實際更新。

Vercel 特定的陷阱和限制

我們自 2024 年初以來一直在 Vercel 上運行此程式。以下是文檔沒有充分強調的事項:

ISR 快取存儲

Vercel 在其邊緣網路快取中儲存 ISR 頁面。截至 2025 年,Vercel 數據快取有一些你應該知道的限制:

  • Pro 計畫:包含的 ISR 快取很慷慨,但在非常高的容量下快取讀/寫有成本
  • 企業版:自訂限制,但你要為此付費
  • 快取項不會永遠存在:即使使用 revalidate: false,Vercel 也可以逐出最近未被訪問的快取項。我們看到在 Pro 計畫上大約 30 天未訪問後頁面從快取中消失。

無伺服器函數持續時間

背景再生成以無伺服器函數形式運行。在 Vercel Pro 上,默認超時為 60 秒(你可以配置最多 300 秒)。如果你的頁面需要超過該時間來再生成 -- 例如,由於你的 CMS 很慢或你正在進行繁重的影像處理 -- 再生成會無聲地失敗,過期頁面會繼續被提供。

我們在從三個不同 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 數據快取 $40-$80 ISR 快取讀/寫
Vercel 總計 $330-$590/月 取決於流量月份
Contentful (CMS) $489/月 他們的 Team 計畫。來自 ISR 再生成的 API 呼叫很快將我們推過免費層。
Upstash Redis (快取) $30/月 添加以減少 CMS API 呼叫
總計 $849-$1,109/月 對於一個每月提供約 2M 頁面瀏覽量的網站

這很昂貴嗎?與傳統伺服器設置相比,它具有競爭力。與靜態 CDN 上的靜態網站相比,它很昂貴。ISR 再生成函數呼叫是最大的可變成本 -- 每次頁面再生成時,這是一個無伺服器函數運行 1-5 秒。

我們與客戶合作,他們探索了 基於 Astro 的方法,用於 ISR 的成本開始超過其好處的內容豐富的網站。對於內容很少變更的網站,使用 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;
    
    // 記錄到你的監控服務
    if (duration > 5000) {
      console.warn(`[ISR] Slow fetch: ${url} took ${duration}ms`);
      // 發送到 Datadog/Sentry/等。
    }
    
    return res;
  } catch (error) {
    console.error(`[ISR] Fetch failed: ${url}`, error);
    // 這很關鍵 -- 如果提取失敗,再生成失敗
    throw error;
  }
}

Vercel 的內置工具

Vercel 的儀表板顯示 ISR 快取命中率和再生成計數。在 分析 標籤中,尋找:

  • 函數日誌中的 快取狀態HITMISSSTALE
  • 無伺服器函數指標中的 ISR 再生成持續時間
  • 你的 ISR 路由上的 錯誤率

`x-vercel-cache` 頁眉

來自 Vercel 的每個回應都包括此頁眉:

  • HIT -- 從邊緣快取提供,新鮮
  • STALE -- 從邊緣快取提供,在背景觸發再生成
  • MISS -- 不在快取中,按需呈現

我們設定了一個簡單的監視器,每小時檢查 100 個隨機頁面,如果超過 10% 返回 MISS,就會發出警報 -- 這將表示快取逐出問題。

架構決策:ISR vs. 替代方案

在大規模運行 ISR 超過一年後,我對何時使用它以及何時不使用的誠實看法是:

何時使用 ISR:

  • 你有 5,000-100,000 個頁面,以不同的頻率變更
  • 內容新鮮度以分鐘計(不是秒)是可接受的
  • 你已經致力於 Next.js
  • 你的團隊理解快取失效(在此規模上它不是可選知識)

何時考慮替代方案:

  • 你需要實時內容(改用 SSR 或客戶端擷取)
  • 你的網站很少變更(完整靜態構建更簡單且更便宜)
  • 你有 500,000+ 頁面(ISR 在非常高的頁面計數上開始不堪重負 -- 考慮分佈式構建方法)
  • 成本是主要關注點(使用你自己的 CDN 自託管 Next.js 的成本可以便宜 60-70%)

對於具有複雜內容架構的客戶,我們經常推薦 無頭 CMS 設置,根據內容類型在 ISR、SSR 和完整靜態之間切換的靈活性。

我們實際使用的混合方法

我們不在我們 25k 頁面網站上的所有內容上使用 ISR。這是分解:

  • ISR:產品頁面、類別頁面、位置頁面(22,000 頁面)
  • SSR:搜尋結果、用戶儀表板、購物車
  • 靜態:關於、聯絡、法律頁面(在構建時生成,無再驗證)
  • 客戶端:實時庫存計數、用戶特定定價

與我們初始的「ISR 一切」策略相比,這種混合方法將我們的無伺服器函數成本降低了約 40%。

來自我們部署的效能基準

以下是我們生產部署在 2025 年第一季度測量的實際數字:

指標 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
核心網頁信號通過率 96% 78% 64%

快取命中和未命中之間的區別是戲劇性的。這是為什麼你的預渲染策略非常重要的原因 -- 你希望你的高流量頁面總是溫暖的。

一個有趣的發現:當我們在低變更內容上從 revalidate: 60 轉移到 revalidate: 3600 時,我們的核心網頁信號分數提高了 12%。更少的再生成意味著更一致的快取命中,這意味著更一致的效能。

常見問題

ISR 在 Vercel 上性能開始降級前可以處理多少頁面? 我們已在 25,000 個頁面上運行而沒有明顯問題,我聽說過有 100,000+ 頁面的部署工作正常。瓶頸不是快取中的頁面數量 -- 這是同時再生成的速率。如果你有 50,000 個頁面都有 revalidate: 60,你會遇到問題。根據內容變更頻率分散你的再驗證期間,你會很好。

ISR 在 Vercel 上的成本是否超過同樣流量的 SSR? 一般來說,ISR 對於相同的流量體積要便宜得多於 SSR。使用 ISR,大多數請求從邊緣快取提供(基本上免費計算)。使用 SSR,每個請求都運行無伺服器函數。對於我們每月 2M 頁面瀏覽量的網站,ISR 的函數呼叫(來自再生成)大約是完整 SSR 會是什麼的 15%。

當 ISR 再生成失敗時會發生什麼? 過期版本繼續被提供。這既是一個功能又是一個風險。你的用戶看不到錯誤,但他們可能看到過期內容。我們遇到過 CMS API 中斷意味著頁面提供的內容已有 6 小時,然後才有人注意到的情況。設定監控。

我可以將 ISR 與 Next.js App Router 一起使用嗎? 可以,而且在 App Router 中實際上更清晰。你在頁面或佈局級別使用 export const revalidate = 60,並在你的提取呼叫中使用 next: { revalidate, tags }generateStaticParams 函數替換 getStaticPaths。我們在本文中描述的一切適用於 Pages Router 和 App Router,儘管對於 2025 年新項目,我們會推薦 App Router 語法。

我如何處理帶有動態查詢參數的 ISR? ISR 只根據 URL 路徑而不是查詢參數進行快取。如果你需要 ?color=red vs ?color=blue 的不同快取版本,你需要使用實際路徑段(/product/widget/red 而不是 /product/widget?color=red)或在客戶端處理變化。這在我們的過濾實現中讓我們感到驚訝。

按需再驗證在規模上可靠嗎? 大多數。我們看到在呼叫 revalidateTag() 和快取實際在所有邊緣位置失效之間偶有 10-30 秒的延遲。對於 99% 的用例,這很好。如果你需要即時全球失效,你可能需要添加快取破壞查詢參數或為這些特定頁面使用 SSR。

我應該自託管 Next.js 而不是使用 Vercel 來處理大型 ISR 網站嗎? 這取決於你的團隊。自託管(例如在 AWS 上)給你對快取行為的更多控制,規模上的成本可以便宜 50-70%。但你負責設定 CDN 快取失效、管理構建管道和自己處理邊緣分佈。我們看到團隊花費數月的時間來複製 Vercel 為你開箱即用提供的功能。如果你想探索選項,與我們聯絡 -- 我們兩者都做過。

什麼是 25,000+ 頁面 ISR 網站的最佳 CMS? 我們在此規模上使用了 Contentful、Sanity 和 Hygraph。Contentful 處理基於 webhook 的再驗證很好,但速率限制可能是一個問題(計畫快取)。Sanity 的 GROQ 訂閱對實時了解內容變更非常有用。Hygraph 的 webhook 系統很穩定。關鍵要求是可靠的 webhook 遞送和可以處理再生成風暴的突發流量的 API。檢查我們的 無頭 CMS 開發能力,以根據你的內容模型獲取更具體的建議。