您在凌晨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 16 cacheComponents:從App Router快取遷移91,000個頁面

我們實際擁有的快取問題

讓我描繪一下這個景象。在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
      },
    },
  },
};

三層模型(stalerevalidateexpire)很好地映射到陳舊-同時-重新驗證語義。在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配合使用效果更好,因為您是標籤化特定快取的元件,而不是嘗試使不透明框架級快取失效。

Next.js 16 cacheComponents:從App Router快取遷移91,000個頁面——架構

我們針對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週內完成。實際的代碼更改很簡單——時間用於審核快取需求、測試失效流程和監控部署後的快取命中率。如果您寧願不單獨進行,請與我們聯繫——我們已經做過幾次了。