在 Vercel 上大規模運行 ISR:使用增量靜態再生部署 25,000+ 頁面
ISR 大規模運行:在 Vercel 上運行 25,000+ 頁面的增量靜態再生成
去年我們在 Vercel 上發佈了一個包含超過 25,000 個靜態生成頁面的 Next.js 網站。產品頁面、部落格文章、位置登陸頁面、動態類別過濾器 -- 應有盡有。增量靜態再生成 (ISR) 的承諾很吸引人:以靜態網站的速度與伺服器渲染內容的新鮮度相結合。說實話嗎?它大多能交付。但在 25,000+ 頁面的規模下,ISR 的表現與你 50 頁行銷網站上的表現完全不同。邊緣情況變成了主要情況。成本不斷上升。文檔中似乎是理論性的快取失效問題變得非常真實。
這是我希望在開始前就存在的文章。這裡的一切都來自生產經驗 -- 真實的指標、真實的帳單驚喜,以及我們做出的真實架構決策(有時還後悔了)。

目錄
- ISR 在幕後實際做什麼
- 為什麼 25,000 個頁面改變了一切
- 構建策略:預渲染什麼 vs. 延遲什麼
- 實際有效的再驗證模式
- Vercel 特定的陷阱和限制
- 大規模實際生產成本
- 生產環境中的 ISR 監控和調試
- 架構決策:ISR vs. 替代方案
- 來自我們部署的效能基準
- 常見問題
ISR 在幕後實際做什麼
在我們深入探討規模問題之前,讓我們確保我們在同一頁面上了解 ISR 是什麼。當你在 Next.js 頁面中設定 revalidate: 60 時,這是實際的流程:
部署後的第一個請求:如果該頁面在構建時預先呈現,Vercel 會從邊緣快取提供該頁面。如果不是(你返回了
fallback: 'blocking'或在 App Router 中使用了dynamicParams: true),它會進行伺服器端渲染,快取結果,然後提供該頁面。再驗證窗口內的後續請求:從快取提供。快速。無計算。
再驗證窗口過期後的第一個請求:過期頁面立即提供(這是「過期重新驗證」部分),並觸發背景再生成。下一個訪問者會獲得新鮮頁面。
這在概念上很簡單。但在 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 最好能夠處理這個。

構建策略:預渲染什麼 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 快取命中率和再生成計數。在 分析 標籤中,尋找:
- 函數日誌中的 快取狀態:
HIT、MISS、STALE - 無伺服器函數指標中的 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 開發能力,以根據你的內容模型獲取更具體的建議。