Next.js 國際化大規模應用:30 種語言、91K 頁面、Vercel ISR
大規模國際化Next.js:30種語言、91K頁面、Vercel ISR
去年,我們交付了一個Next.js專案,每當我想到它時都會有點緊張。三十種語言。超過91,000個靜態生成的頁面。Vercel ISR保持一切新鮮。這是那種一個錯誤的架構決策就意味著你要面對4小時建構、800美元/月主機帳單,或者最糟的情況——一個在韓文中根本不能正常工作的網站的專案。
這是我們如何正確處理它的故事(以及我們最初沒有做對的部分)。如果你正在構建大規模國際化的Next.js應用程式,並想知道ISR是否真的能在生產環境中處理它,這篇文章就是為你而寫的。

目錄
- 問題所在:為什麼91K頁面是一個不同的野獸
- 我們早期做出的架構決策
- 為30個地區設置Next.js i18n
- 實際有效的ISR策略
- 內容管道和無頭CMS集成
- 效能結果和核心網站指標
- Vercel上的成本明細
- 我們犯的錯誤以及如何修復
- 何時使用此堆疊(以及何時不使用)
- 常見問題
問題所在:為什麼91K頁面是一個不同的野獸
讓我設定場景。客戶是一個企業電子商務品牌,正在擴展到30個市場。每個市場需要:
- 本地化的產品頁面(~2,800個產品 × 30個地區 = 84,000個頁面)
- 類別頁面(~120個類別 × 30個地區 = 3,600個頁面)
- CMS驅動的行銷頁面(~120 × 30 = 3,600個頁面)
- 總計:大約91,200個唯一URL
使用純getStaticPaths和完整的靜態生成,初始構建大約需要3到5小時。這不是打字錯誤。我們測試基準早期的原型並觀看數字攀升。每次部署都意味著幾小時的停機風險,內容團隊想要每天多次發佈更新。
SSR也不是一個選項。客戶的流量模式顯示銷售事件期間的大規模峰值——我們談論的是50K併發使用者。在該負載下伺服器渲染91K可能的頁面變體需要認真的計算能力並引入會影響轉換率的延遲。
ISR是答案。但這種規模的ISR有其自己的一套挑戰,Next.js文件並沒有真正為你做好準備。
我們早期做出的架構決策
在編寫單行i18n程式碼之前,我們做出了三項架構決策,稍後為我們節省了數個月的痛苦。
決策1:子路徑路由,而不是網域
Next.js支持兩種i18n策略:子路徑路由(/fr/products/...)和網域路由(fr.example.com)。我們選擇了子路徑路由。原因如下:
| 因素 | 子路徑路由 | 網域路由 |
|---|---|---|
| DNS/SSL複雜性 | 單一網域 | 30個網域/子網域要管理 |
| Vercel部署 | 一個專案 | 一個專案(但網域配置開銷) |
| SEO連結權益 | 在一個網域上鞏固 | 跨網域分割 |
| CDN快取效率 | 更好(共享邊緣快取) | 分割 |
| 分析設定 | 更簡單 | 30個屬性或複雜篩選 |
對於大多數少於50個地區的專案,子路徑路由是正確的選擇。當你因法律原因需要國家特定的頂級網域或當你的市場有根本不同的內容架構時,網域路由是有意義的。
決策2:next-intl而不是next-i18next
我們廣泛評估了這兩個庫。在2025年,next-intl(v4.x)對App Router專案已成為更強的選擇,儘管我們對這個構建使用Pages Router。即使在Pages Router上,next-intl也給了我們:
- 更好的TypeScript支持,具有類型安全的訊息鍵
- 更小的客戶端包(約2.1KB gzip vs ~5KB的next-i18next)
- 對ICU訊息格式的原生支持(複數、性別、數字格式)
- ISR頁面的更簡單配置
決策3:部分靜態生成 + ISR
這是大的。我們不是試圖在構建時靜態生成所有91K頁面,而是預構建只是最高流量的頁面(約8,000個),讓ISR在需要時處理其餘的。
// pages/[locale]/products/[slug].tsx
export async function getStaticPaths() {
// 只預生成前100個產品 × 前5個地區
const topProducts = await getTopProducts(100);
const primaryLocales = ['en', 'de', 'fr', 'es', 'ja'];
const paths = topProducts.flatMap(product =>
primaryLocales.map(locale => ({
params: { slug: product.slug, locale },
}))
);
return {
paths,
fallback: 'blocking', // ISR處理其他一切
};
}
這將我們的構建時間從3小時以上減少到約12分鐘。剩餘的83,000個頁面在第一次請求時生成並在邊緣快取。

為30個地區設置Next.js i18n
next.config.js中的Next.js內建i18n配置處理地區檢測和路由。這是我們的配置看起來的樣子(縮寫):
// next.config.js
const nextConfig = {
i18n: {
locales: [
'en', 'de', 'fr', 'es', 'it', 'pt', 'nl', 'pl', 'cs', 'da',
'sv', 'fi', 'nb', 'hu', 'ro', 'bg', 'hr', 'sk', 'sl', 'el',
'tr', 'ja', 'ko', 'zh-CN', 'zh-TW', 'th', 'vi', 'id', 'ms', 'ar'
],
defaultLocale: 'en',
localeDetection: false, // 我們自己處理這個
},
};
這裡有幾點要注意。我們禁用了localeDetection,因為內建檢測(基於Accept-Language標頭)在ISR快取中引起問題。當Vercel的CDN快取一個頁面時,地區需要從URL確定為確定性的,而不是從標頭。讓Next.js根據瀏覽器語言自動重定向意味著快取未命中和不一致的行為。
相反,我們構建了一個自訂地區檢測中介軟體,只在根路徑上運行:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
const SUPPORTED_LOCALES = ['en', 'de', 'fr', /* ... */];
const DEFAULT_LOCALE = 'en';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 只在根路徑重定向
if (pathname === '/') {
const acceptLanguage = request.headers.get('accept-language') || '';
const preferred = acceptLanguage.split(',')[0]?.split('-')[0] || DEFAULT_LOCALE;
const locale = SUPPORTED_LOCALES.includes(preferred) ? preferred : DEFAULT_LOCALE;
return NextResponse.redirect(new URL(`/${locale}`, request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/'],
};
翻譯檔案結構
使用30種語言,翻譯檔案管理變成一個真正的關注點。我們按命名空間組織翻譯:
messages/
├── en/
│ ├── common.json
│ ├── product.json
│ ├── checkout.json
│ └── marketing.json
├── de/
│ ├── common.json
│ ├── product.json
│ └── ...
└── ar/
└── ...
跨所有語言的總翻譯負荷約為4.2MB。但因為我們使用getStaticProps按頁面加載翻譯,每個單獨頁面只加載其地區和命名空間的15-40KB翻譯資料。這很關鍵——你不想將全部30個地區發送到客戶端。
export async function getStaticProps({ locale }: GetStaticPropsContext) {
return {
props: {
messages: {
...(await import(`../../messages/${locale}/common.json`)).default,
...(await import(`../../messages/${locale}/product.json`)).default,
},
},
revalidate: 300, // ISR:每5分鐘重新驗證
};
}
阿拉伯語的RTL支持
阿拉伯語是我們集合中唯一的RTL語言。我們用一個簡單的佈局包裝器處理它:
const direction = locale === 'ar' ? 'rtl' : 'ltr';
return (
<html lang={locale} dir={direction}>
<body className={direction === 'rtl' ? 'font-arabic' : 'font-sans'}>
{children}
</body>
</html>
);
加上Tailwind的rtl:變體,用於間距和佈局調整。這出乎意料地運作良好——也許我們的CSS中只有5%需要RTL特定的覆蓋。
實際有效的ISR策略
ISR(增量靜態再生)是這個故事的英雄,但在規模上良好使用它需要理解Vercel的基礎設施實際上如何運作。
重新驗證時間
我們根據內容類型使用了不同的重新驗證期:
| 頁面類型 | 重新驗證期 | 原因 |
|---|---|---|
| 產品頁面 | 300秒(5分鐘) | 價格/庫存經常變化 |
| 類別頁面 | 900秒(15分鐘) | 產品列表更新頻率較低 |
| 行銷/CMS頁面 | 3600秒(1小時) | 內容變化是計劃好的 |
| 每個地區的首頁 | 600秒(10分鐘) | 新鮮度和快取的平衡 |
按需重新驗證
對於關鍵更新(價格變化、庫存短缺),我們從無頭CMS透過webhook設定按需重新驗證:
// pages/api/revalidate.ts
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { secret, slug, locales } = req.body;
if (secret !== process.env.REVALIDATION_SECRET) {
return res.status(401).json({ message: 'Invalid secret' });
}
try {
const targetLocales = locales || ['en']; // 如果未指定,預設為英文
const revalidations = targetLocales.map((locale: string) =>
res.revalidate(`/${locale}/products/${slug}`)
);
await Promise.all(revalidations);
return res.json({ revalidated: true, paths: targetLocales.length });
} catch (err) {
return res.status(500).json({ message: 'Error revalidating' });
}
}
一個陷阱:當你重新驗證存在於30個地區的產品時,你進行了30次重新驗證呼叫。對於100個產品的批量更新,那是3,000個重新驗證請求。我們必須添加速率限制並將這些排隊通過無伺服器功能以避免達到Vercel的API限制。
Stale-While-Revalidate模式
ISR的美妙之處在於它在重新生成時提供陳舊的內容。對於這個專案,那意味著使用者總是得到快速的回應(來自Vercel邊緣的快取HTML),即使資料是最多5分鐘舊的。對於電子商務網站,這是一個可接受的權衡——購物車和結帳流程總是命中實時API,用於實時庫存/定價。
內容管道和無頭CMS集成
內容存在於無頭CMS中(Contentful,在這種情況下,儘管我們對其他客戶已經用Sanity和Storyblok進行了類似的設定——有關更多資訊,請參閱我們的無頭CMS開發服務)。
Contentful的本地化模型適用於30個地區。每個條目都有特定地區的欄位值,他們的API支持按地區查詢。但有一個效能考慮:使用全部30個地區的資料取得產品明顯大於取得一個地區。
我們總是在getStaticProps中針對單個地區進行查詢:
const product = await contentfulClient.getEntry(productId, {
locale: mapToContentfulLocale(locale), // 'en-US', 'de-DE'等。
include: 2, // 解析2級連結的條目
});
這使得API回應時間保持在200ms以下,即使對於具有多個引用的複雜產品條目。
翻譯管理
對於UI翻譯(按鈕、標籤、錯誤訊息),我們使用與我們的Git儲存庫集成的Crowdin。工作流程:
- 開發人員將新的英文字串添加到
messages/en/*.json - Crowdin同步並通知譯者
- 翻譯作為PR返回
- CI驗證JSON結構和完整性
- 缺失的翻譯退回到英文
後備策略至關重要。你永遠不想在生產頁面上顯示翻譯鍵,如product.add_to_cart。我們的後備連鎖是:要求的地區 → 語言族(例如,pt-BR → pt) → 英文。
效能結果和核心網站指標
推出後,這是我們在全部30個地區測量的:
| 指標 | 目標 | 實際(P75) | 備註 |
|---|---|---|---|
| LCP | < 2.5秒 | 1.8秒 | ISR快取命中 |
| FID | < 100毫秒 | 45毫秒 | 最小客戶端JS |
| CLS | < 0.1 | 0.03 | 字體加載策略幫助 |
| TTFB | < 800毫秒 | 120毫秒 | Vercel邊緣,快取頁面 |
| TTFB(快取未命中) | < 2秒 | 1.4秒 | ISR在第一次請求時生成 |
| 構建時間 | < 20分 | 11分40秒 | 只預生成8K頁面 |
TTFB數字是這裡的明星。快取頁面120毫秒意味著東京、聖保羅和法蘭克福的使用者都會從附近的邊緣節點獲得快速的回應。快取未命中的1.4秒是ISR生成時間——可以接受,因為它每頁每重新驗證期間只發生一次。
30種語言的字體加載
多語言網站特有的一個效能挑戰:字體。你不能對30種語言使用單一字體族。我們需要:
- 拉丁文/西里爾文:Inter(大多數歐洲語言)
- 阿拉伯文:Noto Sans Arabic
- 中日韓:Noto Sans JP/KR/SC/TC
- 泰文:Noto Sans Thai
使用next/font和按地區字體加載防止不必要的字體下載。訪問日文網站的使用者只下載Noto Sans JP,而不是阿拉伯文或泰文字體。
Vercel上的成本明細
讓我們談談金錢,因為這是大規模ISR變得有趣的地方。以下是我們2025年每月Vercel帳單明細:
| 欄目 | 每月成本 | 備註 |
|---|---|---|
| Vercel Pro計畫 | $20/座位 × 4 | 基本團隊計畫 |
| 頻寬(8TB/月) | ~$320 | 首個1TB後每TB 40美元 |
| 無伺服器函式執行 | ~$180 | ISR再生 + API路由 |
| 邊緣中介軟體執行 | ~$45 | 地區檢測 |
| ISR寫入 | ~$90 | 快取寫入操作 |
| 總計 | ~$715/月 |
對於在30個地區跨月處理2M+頁面檢視的網站,$715是極其合理的。替代方案——在專用基礎設施上運行SSR——的成本為$2,000-4,000/月,以獲得等同的效能和可靠性。
要監視的一件事:如果你觸發大量重新驗證,ISR快取寫入成本可能會激增。我們有一個事件,其中CMS大量發佈觸發了對15,000個頁面的重新驗證。該單一事件的成本約為40美元的額外功能執行。我們現在批量重新驗證呼叫,它們之間延遲100毫秒。
我們犯的錯誤以及如何修復
如果我說這從第一天開始就順利進行,我就在說謊。以下是最大的錯誤:
錯誤1:在構建時生成所有地區
我們的第一種方法試圖預生成每個地區中的每個頁面。構建運行了3小時47分鐘。然後它失敗了,因為Vercel的構建超時(在Pro上)是45分鐘。即使在移到自訂構建伺服器後,部署過程也很糟糕。
修復: 部分預生成,使用fallback: 'blocking'。只構建最重要的頁面,讓ISR處理長尾。
錯誤2:未正確設定`fallback`
我們最初使用了fallback: true而不是fallback: 'blocking'。區別很重要:true在第一次請求時提供骨架/加載狀態,而blocking等待頁面生成。使用true,我們遇到了水合錯誤,因為我們的產品元件期望在後備渲染期間不存在的資料。
修復: 切換到fallback: 'blocking'。第一個訪問未快取頁面的訪問者等待1-2秒,但之後的每個人都立即獲得快取版本。
錯誤3:SEO Hreflang標籤是錯誤的
這是一個容易出錯的問題。Google需要hreflang標籤來理解本地化頁面之間的關係。我們的初始實現缺少x-default標籤,並且在<link>標籤和XML sitemap之間存在不一致。
// 正確的hreflang實現
<Head>
{locales.map(loc => (
<link
key={loc}
rel="alternate"
hrefLang={loc}
href={`https://example.com/${loc}${path}`}
/>
))}
<link rel="alternate" hrefLang="x-default" href={`https://example.com/en${path}`} />
</Head>
錯誤4:Sitemap生成
使用91K URL,單個sitemap XML檔案無法運作(Google的限制是每個sitemap 50,000個URL)。我們需要一個sitemap索引,其中包含多個子sitemap,按地區分割:
<!-- sitemap-index.xml -->
<sitemapindex>
<sitemap><loc>https://example.com/sitemaps/en.xml</loc></sitemap>
<sitemap><loc>https://example.com/sitemaps/de.xml</loc></sitemap>
<!-- ... 再加28個 -->
</sitemapindex>
我們使用next-sitemap和自訂配置生成了這些,並在每次構建時重新生成。
何時使用此堆疊(以及何時不使用)
此架構——Next.js + i18n + Vercel上的ISR——功能強大,但並非對所有情況都是正確的選擇。
在以下情況下使用:
- 你有10+個地區,具有數千個頁面
- 內容更新頻繁但不是實時的
- 效能和核心網站指標對SEO很重要
- 你的團隊對React/Next.js很了解
考慮替代方案,當:
- 你有少於5個地區且少於1,000個頁面(純SSG可能更簡單)
- 內容真正實時(股票交易、實況比分)——使用SSR或客戶端取得
- 你的主機預算受限——對於純靜態多語言網站,考慮Astro,成本為其一小部分
- 你的團隊很小,不需要React的互動性——靜態網站生成器和i18n可能更少維護
對於考慮這樣的專案的團隊,我們已經幫助多個企業客戶架構和構建大規模Next.js應用程式。前兩周的架構決策決定專案是否成功或成為維護惡夢。如果你想討論你的具體情況,請聯繫我們。
常見問題
Next.js i18n路由如何與ISR一起工作?
Next.js i18n路由將地區前綴添加到URL(如/fr/products/shoes)。與ISR結合時,每個地區+頁面組合在Vercel的邊緣獨立快取。所以/en/products/shoes和/fr/products/shoes是單獨的快取條目,每個都有自己的重新驗證計時器。getStaticProps函式在其上下文中接收地區,你在那裡取得適當的翻譯和本地化內容。
Next.js ISR在Vercel上最多可以處理多少頁面? Vercel能夠提供ISR頁面數量沒有硬技術限制。我們已經成功運行91K+頁面,我聽說過500K+頁面的專案。實際限制是構建時間(針對預生成的頁面)、重新驗證吞吐量和成本。Vercel的邊緣快取設計用於此規模——它本質上是具有智慧無效化的CDN。
ISR是否影響多語言網站的SEO?
不會,ISR頁面在從快取提供時是完全渲染的HTML,這是搜尋引擎爬蟲看到的。關鍵的SEO考慮是適當的hreflang標籤、結構良好的sitemap索引,其中包含每個地區的sitemap,以及確保你的fallback: 'blocking'設定防止爬蟲看到不完整的頁面。Google已確認ISR/快取頁面的處理方式與傳統靜態HTML相同。
你如何在不重新部署的情況下處理翻譯更新? 對於CMS管理的內容(產品描述、行銷文案),翻譯會透過ISR重新驗證自動更新——無論是在計時器上還是透過按需重新驗證webhook。對於UI字串翻譯(按鈕標籤、表單驗證訊息),這些在構建時捆綁,所以它們需要重新部署。我們有意保持這些分開:內容變化應該永遠不需要部署,但UI字串變化通過程式碼審查。
Vercel上ISR和SSR對於多語言網站的成本差異是什麼? SSR在每個單一請求上執行無伺服器功能。在月2M頁面檢視中,這是2M功能調用,在免費等級後大約$0.40/百萬——月約$800的功能成本,加上明顯更高的頻寬,因為快取更少。我們的ISR設定的成本約為每月$715總計,而等同的SSR將是$2,500-3,500/月。
你如何跨30個地區處理不同的日期、數字和貨幣格式?
我們透過next-intl的格式化實用程式使用瀏覽器內建的Intl API。這為每個地區正確處理日期格式(Intl.DateTimeFormat)、數字格式(Intl.NumberFormat)和貨幣顯示。ICU訊息格式讓你直接在翻譯字串中嵌入這些格式化程式:"price": "From {amount, number, ::currency/EUR}"。這在ISR生成期間和客戶端針對動態值都有效。
我應該對大規模i18n使用App Router或Pages Router嗎?
自Next.js 15(中期2025)以來,App Router的i18n故事已經成熟,next-intl v4具有出色的App Router支持。對於新專案,我建議使用App Router。它提供更好的流媒體、React伺服器元件(減少客戶端JavaScript),以及更細粒度的快取控制。我們的專案使用了Pages Router,因為它是在2024年開始的,當時App Router i18n不太穩定,但今天的新建專案應該去App Router。
如果ISR重新驗證失敗會發生什麼?使用者看到錯誤頁面嗎?
不是,這是ISR的最佳功能之一。如果重新驗證失敗(也許CMS API已關閉,或getStaticProps中有程式碼錯誤),Vercel會繼續提供上次成功生成的頁面版本。使用者永遠不會看到錯誤——他們只是看到略微陳舊的內容。失敗的重新驗證被記錄,下一個重新驗證嘗試將再次嘗試。與SSR相比,這使ISR極其可靠,其中API中斷立即成為面向使用者的中斷。