Next.js 16 cacheComponents:無停機時間遷移91,000個頁面
您在凌晨2:14部署。九萬一千個產品頁面——分類樹、跨14個市場的本地化變體、編輯SEO內容——從Next.js 14的App Router切換到v16的cacheComponents API。您在Vercel的日誌中觀看第一個請求瀑布。TTFB激增至1.8秒。您的Slack發出警告。在過去的十八個月裡,您一直在與舊的默認快取模型作鬥爭:過期定價、revalidateTag調用過晚、客戶支持票據指責「網站」。Next.js 15默認關閉了快取;v16讓您透過cacheComponents有選擇地重新啟用。您選擇遷移勝過死於千次快取錯誤。現在您正盯著真實流量、真實錯誤,以及距離日出兩小時的生產事件。以下是什麼破裂、什麼堅持,以及沒有基準測試套件預測的性能差異。
目錄
- 我們實際擁有的快取問題
- Next.js 15和16中的變化
- 了解cacheComponents
- 我們針對91,000個頁面的遷移策略
- 實施:逐步
- 性能結果和基準
- 陷阱和注意事項
- 何時應該和不應該使用cacheComponents
- 常見問題

我們實際擁有的快取問題
讓我描繪一下這個景象。在Next.js 14的App Router中,伺服器元件中的fetch請求默認被快取。數據快取在部署之間持久化。完整路由快取在構建時存儲渲染的HTML和RSC有效載荷。客戶端上的路由器快取讓預取的段保持在……好吧,比你預期的時間更長。
對於91,000個頁面的網站,這種默認快取一切的方法造成了兩類問題:
到處都是過期數據。 產品價格在我們的無頭CMS(在我們的例子中是Sanity)中更新,但快取的fetch結果卡住了。我們散佈了47個不同伺服器操作中的revalidateTag調用。漏掉一個標籤?客戶看到昨天的價格。我們確實有一個名為#cache-crimes的Slack頻道,內容團隊在其中報告過期頁面。
來自地獄的構建時間。 完全靜態生成91,000個頁面花費了超過3小時。我們已經移動到ISR,大多數頁面的revalidate: 3600,但ISR、數據快取和按需重新驗證之間的交互非常難以理解。團隊中的新開發人員會花費他們的前兩周來理解快取層。
心智模型的成本
以下是我認為人們低估的:隱式快取的認知成本。當快取是默認的並且您退出時,每個新元件都需要您問「這應該被快取嗎?」然後記住在答案是否定時添加正確的指令。當無快取是默認的並且您選擇加入時,您只在主動想要快取時才思考快取。這是根本上不同的——更好的——心智模型。
Next.js 15和16中的變化
Next.js 15是大的哲學轉變。團隊翻轉了默認值:
| 行為 | Next.js 14 | Next.js 15 | Next.js 16 |
|---|---|---|---|
伺服器元件中的fetch() |
默認快取 | 默認不快取 | 默認不快取 |
| 路由處理程序(GET) | 默認快取 | 默認不快取 | 默認不快取 |
| 客戶端路由器快取 | 30秒(動態)/ 5分鐘(靜態) | 頁面段為0秒 | 0秒默認,可配置 |
| 完整路由快取 | 針對靜態路由啟用 | 相同 | 相同,帶有cacheLife細化 |
| 元件級快取 | unstable_cache |
use cache指令(實驗性) |
cacheComponents API(穩定) |
Next.js 15在標誌後推出了use cache指令作為實驗性功能。Next.js 16(2025年初發佈)將其穩定為cacheComponents配置選項和相關的"use cache"指令,以及用於定義自定義快取配置文件的cacheLife和用於有針對性失效的cacheTag。
關鍵洞察:快取從隱式框架行為轉移到元件級別的顯式開發人員選擇。 這對大型網站來說是一個巨大的交易。
了解cacheComponents
next.config.js中的cacheComponents功能透過"use cache"指令啟用元件級快取。以下是基本設置:
// next.config.js (Next.js 16)
const nextConfig = {
experimental: {
cacheComponents: true,
},
};
module.exports = nextConfig;
啟用後,您可以將"use cache"添加到任何異步伺服器元件、伺服器操作,甚至佈局文件的頂部:
// app/products/[slug]/page.tsx
"use cache";
import { cacheLife, cacheTag } from 'next/cache';
export default async function ProductPage({ params }: { params: { slug: string } }) {
cacheLife('products'); // custom cache profile
cacheTag(`product-${params.slug}`);
const product = await fetchProduct(params.slug);
return (
<div>
<h1>{product.name}</h1>
<ProductDetails product={product} />
<DynamicPricing productId={product.id} /> {/* This component is NOT cached */}
</div>
);
}
cacheLife配置文件
這就是大型網站變得有趣的地方。您在next.config.js中定義命名的快取配置文件:
const nextConfig = {
experimental: {
cacheComponents: true,
cacheLife: {
products: {
stale: 300, // serve stale for 5 minutes
revalidate: 3600, // revalidate after 1 hour
expire: 86400, // hard expire after 24 hours
},
editorial: {
stale: 3600,
revalidate: 86400,
expire: 604800, // 7 days
},
navigation: {
stale: 86400,
revalidate: 604800,
expire: 2592000, // 30 days
},
},
},
};
三層模型(stale、revalidate、expire)很好地映射到陳舊-同時-重新驗證語義。在stale窗口內,快取的內容立即提供。在stale之後但在expire之前,背景重新驗證啟動。在expire之後,快取條目消失。
cacheTag用於失效
cacheTag函數以更可組合的方式替換舊的revalidateTag模式:
import { revalidateTag } from 'next/cache';
// In a webhook handler or server action:
export async function handleProductUpdate(productSlug: string) {
revalidateTag(`product-${productSlug}`);
revalidateTag('product-listing'); // invalidate listing pages too
}
這部分與Next.js 15相比沒有太大變化,但它與cacheComponents配合使用效果更好,因為您是標籤化特定快取的元件,而不是嘗試使不透明框架級快取失效。

我們針對91,000個頁面的遷移策略
我們沒有一步完成。跨14個地區有91,000個頁面,大爆炸遷移將是魯莽的。以下是我們如何分解的:
階段1:升級到Next.js 16,無快取變化(第1-2週)
我們從Next.js 14.2升級到16.0,沒有啟用cacheComponents。這單獨改變了行為,因為fetch請求不再默認被快取。我們預期TTFB回歸,我們得到了它們:
- 平均TTFB從180ms上升到340ms在產品頁面上
- 源伺服器負載增加約60%(我們的Sanity CDN表現良好,但我們的自定義API端點沒有)
- ISR重新驗證實際上變快了因為管理的快取狀態較少
這確認了我們的懷疑:我們一直在依賴隱式快取,許多頁面確實需要快取——只是明確、有意的快取。
階段2:審核和分類頁面(第3週)
我們對應用中的每個路由進行了分類:
| 頁面類型 | 數量 | 快取策略 | cacheLife配置文件 |
|---|---|---|---|
| 產品詳情頁面 | 42,000 | 使用產品標籤快取 | products(5分鐘過期/ 1小時重新驗證) |
| 分類列表頁面 | 3,200 | 使用分類標籤快取 | products(5分鐘過期/ 1小時重新驗證) |
| 編輯/博客頁面 | 8,400 | 積極快取 | editorial(1小時過期/ 24小時重新驗證) |
| 本地化變體 | 31,647 | 與基頁相同 | 從基頁繼承 |
| 帳戶/動態頁面 | 6,000 | 無快取 | N/A |
階段3:啟用cacheComponents,添加指令(第4-6週)
我們啟用了標誌並開始添加"use cache"指令。關鍵決定:我們為大多數路由在頁面級別快取,但為具有混合靜態/動態內容的頁面在元件級別快取。
對於產品頁面,產品信息和圖像被快取,但定價元件和庫存狀態保持未快取:
// components/ProductInfo.tsx
"use cache";
import { cacheLife, cacheTag } from 'next/cache';
export async function ProductInfo({ slug }: { slug: string }) {
cacheLife('products');
cacheTag(`product-${slug}`, 'product-info');
const product = await getProduct(slug);
return (
<section>
<h1>{product.name}</h1>
<p>{product.description}</p>
<ProductImages images={product.images} />
</section>
);
}
// components/DynamicPricing.tsx
// NO "use cache" directive -- always fresh
export async function DynamicPricing({ productId }: { productId: string }) {
const pricing = await getPricing(productId); // hits pricing API every request
return (
<div className="pricing">
<span className="price">${pricing.current}</span>
{pricing.onSale && <span className="was-price">${pricing.original}</span>}
</div>
);
}
階段4:Webhook集成(第7週)
我們重新佈線了Sanity Webhook以使用正確的標籤調用revalidateTag。這實際上比我們的舊設置更簡單,因為標籤現在在代碼中是明確的,不是散佈在fetch選項中。
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const body = await request.json();
const secret = request.headers.get('x-webhook-secret');
if (secret !== process.env.REVALIDATION_SECRET) {
return new Response('Unauthorized', { status: 401 });
}
switch (body._type) {
case 'product':
revalidateTag(`product-${body.slug.current}`);
revalidateTag('product-listing');
break;
case 'category':
revalidateTag(`category-${body.slug.current}`);
revalidateTag('navigation');
break;
case 'article':
revalidateTag(`article-${body.slug.current}`);
break;
}
return new Response('OK', { status: 200 });
}
實施:逐步
如果您進行類似的遷移,以下是我們建議的實踐教科書(以及我們現在在Social Animal用於Next.js開發項目的方法):
步驟1:啟用標誌
// next.config.js
module.exports = {
experimental: {
cacheComponents: true,
cacheLife: {
// Start with sensible defaults
default: {
stale: 60,
revalidate: 900,
expire: 86400,
},
},
},
};
步驟2:找到您的熱路徑
使用您的分析來識別獲得最多流量的頁面和TTFB最重要的地方。對於我們來說,這是分類頁面(高流量、相對穩定的內容)和產品頁面(高流量、中度動態內容)。
步驟3:從上到下添加`"use cache"`
從佈局開始。如果您的根佈局獲取導航數據,首先快取——這是最高影響、最低風險的變化:
// app/layout.tsx
// Note: "use cache" on layouts caches the layout shell
// Child pages still render independently
import { Navigation } from '@/components/Navigation';
export default async function RootLayout({ children }) {
return (
<html>
<body>
<Navigation /> {/* This component has its own "use cache" */}
{children}
</body>
</html>
);
}
步驟4:設置監控
我們使用Vercel的內置分析加上自定義日誌來追蹤快取命中率。在啟用cacheComponents的第一周後,我們的快取命中率只有34%。調整stale持續時間後,它攀升至78%。
性能結果和基準
以下是Vercel專業計畫上30天內測量的完整遷移後的實際數字:
| 指標 | 之前(Next.js 14) | 階段1後(v16,無快取) | 完整遷移後 |
|---|---|---|---|
| 平均TTFB(產品頁面) | 180ms | 340ms | 95ms |
| 平均TTFB(分類頁面) | 220ms | 410ms | 72ms |
| 平均TTFB(編輯頁面) | 150ms | 280ms | 45ms |
| P99 TTFB(所有頁面) | 1,200ms | 2,100ms | 380ms |
| 構建時間(完整) | 3小時12分鐘 | 2小時48分鐘 | 48分鐘 |
| Vercel功能調用/天 | 2.4M | 3.8M | 1.1M |
| 每月Vercel賬單 | ~$840 | ~$1,200 | ~$520 |
| 快取命中率 | 未知(隱式) | N/A | 78% |
| 過期內容事件(#cache-crimes) | 8-12/週 | 0 | 1-2/月 |
構建時間的改進值得解釋。使用cacheComponents,我們遠離了在構建時生成所有91,000個頁面。相反,我們只靜態生成了前5,000個頁面(按流量),讓其餘的按需生成快取。cacheComponents指令意味著那些按需頁面在首次訪問後被快取,cacheLife控制過期。
Vercel賬單下降很明顯。更少的功能調用(因為明確的元件快取)加上更短的構建時間意味著真實的成本節省。那約$320/月的減少付出了代價。
陷阱和注意事項
序列化邊界
"use cache"指令創建了一個序列化邊界。作為道具傳遞到快取元件中的所有內容必須是可序列化的。我們有幾個接收回調函數或React元素作為道具的元件——那些立即破裂。修復是重組為改用組合模式:
// ❌ This breaks with "use cache"
"use cache";
export async function ProductCard({ product, onAddToCart }) {
// onAddToCart is a function -- not serializable!
}
// ✅ This works
"use cache";
export async function ProductCard({ product }) {
return (
<div>
<h2>{product.name}</h2>
{/* AddToCart is a Client Component, not cached */}
<AddToCartButton productId={product.id} />
</div>
);
}
動態參數和快取鍵爆炸
有91,000個頁面,每個都有唯一參數,快取鍵空間是巨大的。我們在第一周內達到了Vercel邊緣快取限制,不得不更有策略性地決定哪些頁面獲得長的expire值。低流量長尾頁面獲得了較短的快取持續時間。
`Date.now()`陷阱
使用"use cache"的任何調用Date.now()或new Date()在快取函數內部的元件都會快取該時間戳。我們在一個「最後更新」顯示中發現了這一點,該顯示在幾個小時內顯示相同的時間。修復:將時間敏感邏輯移動到客戶端元件或未快取的伺服器元件。
嵌套快取邊界
當您在其他快取元件內嵌套快取元件時,內部快取有自己的生命週期。這很強大但令人困惑。我們建立了一個團隊慣例:在頁面級別或元件級別快取,不能同時在兩者,除非有明確的理由。
何時應該和不應該使用cacheComponents
在以下情況下使用它:
- 您有超過幾百個頁面,ISR構建時間令人痛苦
- 您的內容具有清晰的新鮮度要求,按部分變化
- 您需要對快取內容與始終新鮮進行精細控制
- 您在Vercel或支持Next.js快取層的平台上運行
- 您想減少高流量網站上的基礎設施成本
在以下情況下不使用它:
- 您的網站很小,完整的SSG可以很好地工作
- 每個頁面都是完全動態的(到處都是用戶特定的內容)
- 您不在支持Next.js快取基礎設施的託管平台上
- 您的團隊對Next.js不熟悉——首先熟悉基礎知識
如果您正在評估您的項目是否需要這個級別的快取控制,或者不同的框架如Astro對於您的內容重型網站可能更合適,在提交遷移之前值得思考。
對於來自多個無頭CMS源的內容的項目,Next.js 16中的cacheTag系統與無頭CMS架構配合得很好——每個內容類型獲得自己的失效通道。
常見問題
Next.js 16中的cacheComponents是什麼?
cacheComponents是Next.js 16中的實驗性配置選項,啟用伺服器元件的"use cache"指令。它讓您明確標記應該快取哪些元件,並使用cacheLife定義自定義快取配置文件。它是在Next.js 15中實驗性的use cache指令的穩定演變。
cacheComponents與ISR(增量靜態再生)有何不同?
ISR快取整個頁面並在時間表上重新驗證。cacheComponents讓您快取頁面內的各個元件,每個都有不同的快取生命週期。單個頁面可以有一個快取24小時的標題、一個快取1小時的產品信息,以及永不快取的定價。ISR做不到——它在頁面級別上是全有或全無。
我需要在Vercel上才能使用cacheComponents嗎?
沒有,但Vercel上的體驗最好,因為快取基礎設施與框架緊密集成。自託管的Next.js部署可以使用cacheComponents與文件系統快取適配器,但您不會獲得邊緣分佈的好處。Netlify和Cloudflare等平台正在添加支持,但截至2026年中期,Vercel仍然是最完整的實施。
我如何在Next.js 16中使快取的元件失效?
您在快取元件內使用cacheTag()分配標籤,然後從伺服器操作、路由處理程序或Webhook端點調用revalidateTag('tag-name')。這使所有具有該標籤的快取元件失效。這與Next.js 15的API相同,但現在更有用,因為您是標籤化明確快取的元件,而不是嘗試使不透明框架級快取失效。
cacheComponents會降低我的Vercel賬單嗎? 它可以顯著降低成本。在我們的例子中,功能調用下降了54%,因為快取的元件響應是從快取層提供的,而不是調用無伺服器功能。構建時間的減少也節省了構建分鐘數。您的里程將根據流量模式和快取命中率而異——使用您當前的使用情況檢查Vercel的定價計算器。
如果我將"use cache"添加到接收不可序列化道具的元件中會發生什麼?
您將獲得構建錯誤。"use cache"指令創建序列化邊界,因此所有道具必須是可序列化的(字符串、數字、純對象、數組)。函數、React元素、類實例和其他不可序列化的值將導致構建失敗。重組您的元件以僅接受數據道具,並在子客戶端元件中處理交互性。
我可以使用來自其他框架的React伺服器元件的cacheComponents嗎?
不。cacheComponents是一個Next.js特定的功能,它構建在React的伺服器元件之上。雖然"use cache"指令語法最終可能成為React標準,但cacheLife配置文件和cacheTag系統是Next.js API。如果您使用Remix之類的框架或自定義RSC設置,您將需要不同的快取策略。
將大型Next.js網站遷移到cacheComponents需要多長時間? 對於我們具有4名開發人員團隊的91,000頁網站,完整遷移花費了7周,包括測試和監控。具有較簡單數據模型的較小網站(少於10,000頁)可能可以在1-2週內完成。實際的代碼更改很簡單——時間用於審核快取需求、測試失效流程和監控部署後的快取命中率。如果您寧願不單獨進行,請與我們聯繫——我們已經做過幾次了。