next-cache-components-optimizer

next-cache-components-optimizer

熱門

透過設定代理循環,在 Cache Components / PPR 下,讓 Next.js 路由達到即時導航(初始載入(硬導航)與客戶端導航(軟導航))。將目標編碼為失敗的 @next/playwright instant() e2e 測試,並逐一驗證路由,直到測試通過;通過的測試可防止回歸。當被要求讓路由的導航變為即時(其靜態殼層立即提交)、修復靜態殼層未預先渲染/提供/預取的路由、擴大路由的靜態殼層或修復其緩慢的首次繪製、診斷哪個 Suspense 邊界阻止路由進入靜態殼層,或為某個路由編寫 instant() e2e 防護時使用。需要 Next.js 16.3+ 並啟用 cacheComponents;若版本較舊則引導升級。

14萬星標
3.1萬分支
更新於 2026/7/22
SKILL.md
readonlyread-only
name
next-cache-components-optimizer
description

Drive a Next.js route to instant navigation by setting up an agentic loop, under Cache Components / PPR, on initial load (hard navigation) and client-side navigation (soft navigation). Encode the goal as a failing @next/playwright instant() e2e and work it to green, one verified route at a time; the shipped test then guards against regression. Use when asked to make a route's navigation instant (its static shell commits immediately), fix a route whose static shell isn't prerendered/served/prefetched, grow a route's static shell or fix its slow first paint, diagnose which Suspense boundary keeps a route out of its static shell, or write the instant() e2e guard for one. Requires Next.js 16.3+ with cacheComponents; directs an upgrade if older.

next-cache-components-optimizer

建立一個代理優化循環,將 Next.js 路由從「非即時」驅動到「即時」並保持狀態。循環以測試驅動:將目標編碼為失敗的 @next/playwright instant() 測試,使其通過,並將測試作為回歸防護提交。每個目標路由執行一次。依序執行階段 P → G;每個階段結束於一個閘門。修復方案位於兩個懶加載的參考文件中 — reference/patterns.md(每種阻塞類型的 before→after)和 reference/real-app-patterns.md(平行路由、認證閘門、空殼層與響應式骨架失敗模式)。僅在對應階段指向時才讀取其中一個。

什麼是不變的,什麼是你的

這裡有一件事是固定的。其餘都是你的。在將任何命令、平台或環境變數視為需求之前,請先閱讀此內容。

  • 不變項:驗證循環。 最大化殼層是沒有價值的,除非你能證明它。證明是一個自動化檢查:在鎖定動態資料的條件下,靜態殼層仍然提交。RED 顯示差距,GREEN 顯示差距已關閉,測試作為回歸防護提交。它必須在類似生產環境的建置上運行,且不能空洞地通過。循環只需建立一次;之後的每個最佳化都可以透過建構來驗證。循環是交付物,而不是任何單一路由。
  • 機制:@next/playwright instant() 此技能使用 instant() 進行鎖定:這是一把尺,而不是碼表(階段 A)。它來自 @next/playwright(與 @playwright/test 一起安裝,與 next 處於相同發佈線),因此不依賴於任何主機。保留它。手動計時導航過於不穩定而不可信,而這正是此技能旨在防止的失敗模式。
  • 你的:測試框架。 如何建置、部署、認證、配置 Playwright 以及循環,都屬於你的技術棧,而不是此技能。本地的 next build && next start、CI/暫存容器以及每次推送的預覽部署都是同樣有效的框架;結論來自建置,而非平台。階段 0 將不變項映射到你的儲存庫。將下面的每個平台名稱、環境變數拼寫和命令視為需要轉換的範例,而非需求。

兩種導航,兩種載入狀態

路由以兩種方式到達使用者,兩者都必須是即時的:

  • 初始載入(硬導航) 提交路由的預先渲染靜態殼層;延遲部分在其載入骨架(Suspense fallback、loading.tsx)後面串流進入。
  • 客戶端導航(軟導航) 提交目標的預取應用程式殼層 — 部分預取下的 <Link> 預設行為 — 僅重新渲染變更的區段。

兩種的修復模式相同;測試僅在導航驅動方式上有所不同(下面的「在測試中驅動導航」)。兩個殼層可能不同;保護你提交的那個,當兩者都重要時保護兩者(reference/real-app-patterns.md)。

目標

最大化靜態殼層是最佳化目標:最有意義的預先渲染內容立即提交,只有真正每次請求才需要的資料隨後串流進入。提交的測試確定性地編碼了 呈現 ∧ 即時非空白 是工作流程透過判斷(D1/D2/E)強制執行的額外標準,因為單獨通過 instant() 會被空白 fallback={null} 殼層滿足(空殼層失敗模式,reference/real-app-patterns.md)。

instant() 是一把尺,而不是碼表:斷言殼層在鎖定下出現;不要計時。可信的結論需要生產建置(階段 A)。

鎖定下的 GREEN 是確定性的結論;每個閘門保持其可信度。

向使用者報告

此循環旨在無人值守運行 — 理想情況下在一次運行中涵蓋多次導航 — 因此它不會在每個路由後停下來詢問。重要的是你如何措辭和呈現結果,而不是你打斷的頻率。下面的機制 — 框架、RED、GREEN、閘門 — 是你的支架;使用者永遠不需要聽到這些詞。

  • 說他們的語言。 以使用者看到的內容來描述差距和結果:「導航到儀表板時,在圖表查詢完成之前沒有任何內容繪製;現在佈局和骨架立即繪製,圖表串流進入」— 而不是 RED/GREEN、鎖定或階段字母。
  • 展示,而非告知。 當你報告路由時,驅動瀏覽器(或附加前後截圖),讓使用者觀看殼層立即提交和資料串流進入,而不是閱讀聲明。前後相同表示修復無效 — 回滾它。
  • 將一次運行呈現為結果列表, 每行一個導航 — 哪個路由、現在什麼是即時的、什麼串流 — 而不是循環的逐字記錄。
  • 僅在真正的分歧點提出問題: 會改變行為的修復、安全敏感的讀取,或設計上為動態的路由(運行時預取候選,而非要擴大的殼層)。一個乾淨的即時修復不是分歧點 — 繼續前進。在無人可問的情況下(無人值守運行),不要阻塞:採取安全的預設值並記錄假設 — 對於快取新鮮度選擇,將讀取延遲到 <Suspense> 之後(始終新鮮,仍然即時),而不是猜測 cacheLife

工作流程

- [ ] P  先決條件    Next.js 16.3+ 並啟用 cacheComponents: true;先升級 → 下方
- [ ] 0  設定        每個儲存庫一次:發現 + 撰寫 instant-nav.rig.md     → rig-template.md
- [ ] A  框架        暴露測試 API 的生產建置          → 下方
- [ ] B  基準線      未鎖定:標記為測試使用者渲染         → test-template.md
- [ ] C  RED         鎖定 instant():殼層未提交            → test-template.md
- [ ] C-閘門         驗證-RED:停止直到 RED 可信          → reference/red-test-robustness.md
- [ ] D  修復        將每個 Suspense 邊界向下推到它所保護的資料 → reference/patterns.md
- [ ]      D1 重用路由現有的載入 UI;不要手動建立骨架
- [ ]      D2 殼層在每個斷點匹配真實渲染  → reference/real-app-patterns.md
- [ ] E  一致性      重構僅改變了路由是否即時
- [ ] F  差異測試    僅還原修復 → RED;重新應用 → GREEN            → reference/red-test-robustness.md
- [ ] G  審查        PR 檢查清單(下方)

階段 B 和 C 建立測試;僅從 C 提交鎖定測試。


P. 先決條件:當前 Next.js 與 Cache Components

工作流程依賴於當前 Next.js 提供的框架功能:

  • Next.js 16.3+ 並在 next.config.ts 中啟用 cacheComponents: true。沒有 Cache Components 就沒有可最佳化的靜態殼層。
  • @next/playwright 與專案的 next 處於相同發佈線;它提供 instant()。使用 npm ls next @next/playwright(或專案的套件管理器)驗證,如果不同則對齊它們。匹配的測試 API 位於 next 運行時,由 experimental.exposeTestingApiInProductionBuild 配置標誌控制(階段 A)。

如果專案不符合這些條件,請先升級(npx @next/codemod upgrade 自動化大部分工作),然後在 next.config.ts 中啟用 Cache Components:

export default { cacheComponents: true }

啟用標誌會先顯示需要解決的阻塞路由;next-cache-components-adoption 技能驅動該採用。一旦應用程式在 Cache Components 下建置成功,再使用此最佳化器。

此閘門是故意的:技能針對當前 Next.js,而舊版本上的任何結論都沒有意義。

0. 設定:發現此專案的框架,每個儲存庫一次

此技能中的原則是固定的;它們運行的基礎設施是你的。在儲存庫中首次使用時,發現專案如何建置、部署、認證和測試(先檢查儲存庫,僅詢問使用者無法回答的內容),然後將答案寫入已提交的 instant-nav.rig.md。後續每次運行都讀取該文件,而不是重新發現。六個問題(BUILD / EXPOSE / RUN / TEST USER / DRIFT / LOOP)、文件模板以及填寫的範例(僅本地、通用 CI + 容器、預覽部署)位於 rig-template.md

如果儲存庫還沒有 Playwright e2e 測試框架,建立一個最小的框架(@next/playwright、包含 baseURL 的配置、一個經過認證的路徑)是此步驟的一部分;循環不假設預先存在的測試套件。

A. 框架:暴露測試 API 的生產建置

啟動 instant-nav.rig.md 描述的框架。每個平台都有兩個不變項:

  1. 永遠不要在 next dev 上測量。 它不會預取,且其鎖定對於阻塞路由不可靠,因此開發環境的 instant() 結果不是有效的 RED 或 GREEN。

  2. 被測量的建置必須暴露測試 API。 否則 instant() 會默默地無操作,測試空洞地通過(請參閱 reference/red-test-robustness.md)。鎖定接合的證明是階段 C 的 RED 本身:未修復的目標路由是已知的阻塞路由,其在鎖定下的 RED 顯示鎖定在此建置上接合(C-閘門);test-template.md 中的自我驗證變體是帶內保證。將 experimental.exposeTestingApiInProductionBuild 連接到一個條件,該條件對你測量的每個建置都為真,且在生產環境中永遠不為真:

    experimental: {
      // 使用你的平台提供的條件,並記錄在框架文件中:
      //   本地:       明確選擇加入,如下所示
      //   通用 CI:  process.env.DEPLOY_ENV === 'staging'
      //   Vercel:      process.env.VERCEL_ENV === 'preview'
      exposeTestingApiInProductionBuild:
        process.env.EXPOSE_TESTING_API === '1',
    }
    

框架是任何暴露測試 API 的類似生產環境的建置:本地 next build && next start、CI/暫存容器以及預覽部署都是同樣有效的;結論來自建置,而非平台。請參閱 rig-template.md 以取得填寫的範例。

對於任何已部署或遠端建置,在信任結論之前,輪詢框架的 LIVENESS 探測以確認工件包含 HEAD(過時的部署會讀取為虛假的 RED 或 GREEN);本地 next build && next start 不需要。探測機制在 rig-template.md 中(問題 6)。

B. 基準線(未鎖定):開發支架,不要提交

在沒有 instant() 鎖定的情況下驅動真實導航,並斷言目標的 SHELL_MARKER測試使用者 的身份渲染:e2e 套件認證的帳戶(在 CI 中為 CI 帳戶;本地為你的 e2e 登入 fixture),包含其標誌、方案、角色和資料。這確認標記是真實且可達的:未被標誌保護、未被重定向、不是猜測的選擇器。套件以測試帳戶運行,而不是作者的工作階段;該環境漂移(框架 DRIFT 列表)是不可信 RED 的常見來源。支架和運行命令:test-template.md
在 PR 之前刪除此基準線。

C. RED(鎖定)+ 驗證-RED 閘門

將相同的導航包裝在 instant() 中;斷言殼層在鎖定下提交。這裡的 RED 就是差距。這是提交的測試test-template.md)。

C-閘門:在 RED 被驗證為可信之前,不要開始最佳化。 因錯誤原因而變紅的 RED 會讓你最佳化一個從未損壞的路由。

解決這個問題的問題:SHELL_MARKER 在沒有鎖定的情況下,以測試使用者的身份渲染嗎? 透過重新運行階段 B 作為測試使用者來回答,而不是向提交的測試添加斷言。兩個分支的解決方案(否 → 標記或環境錯誤;是 → 真正的差距,繼續進行 D)、不可信 RED 的完整分類、檢查清單以及處理過的案例都在 reference/red-test-robustness.md 中。立即閱讀。


D. 修復:將每個邊界向下推到它所保護的資料

反模式:一個粗略的邊界。 樹中高層的單一 <Suspense> 搭配頁面級別的 fallback 有三個成本:

  • 佈局 UI 不在靜態殼層中:只有它的可拋棄副本被預先渲染。
  • 當邊界解析時,整個子樹被替換,這會丟棄客戶端狀態並導致佈局偏移。
  • 手動建立的 fallback 會隨著 UI 變化而不同步,因為它複製了解析樹中也存在的結構。

修復:提升靜態部分,將 Suspense 向下推。 在殼層中同步渲染佈局 UI 一次,並將每個 await 包裝在一個僅保護該單一讀取的邊界中。只有那個葉子串流;穩定的祖先保持不變。

規則: 如果一個元素在 fallback 和解析樹中都渲染,則將其提升到邊界之上。

最常見的阻塞器:fallback 路由上佈局中的頂層 await

app/[locale]/(app)/[tenant]/dashboard/...
       │ generateStaticParams ✅   │ 無 generateStaticParams → fallback 路由

當路由中的任何動態區段缺少 generateStaticParams 時,該路由是 fallback 路由,並且 所有 參數都延遲到請求時,包括已列舉的參數。佈局中的頂層 awaitawait params、請求時的工作階段讀取、認證閘門)然後阻塞整個子樹,使其無法進入靜態殼層,即使它讀取一個靜態已知的參數。最小形狀:一個動態區段路由,其中一個區段缺少 generateStaticParams,加上其上方佈局中的頂層 await

修復:延遲閘門,渲染 children

無條件渲染 children;將頂層 await 移動到一個由 <Suspense fallback={null}> 包裝的子組件中。機制和 before→after:reference/real-app-patterns.md,「延遲認證閘門」。

也修復殼層下方的頁面,而不僅僅是佈局。 頁面級別的頂層 await(通常是 await params)以與佈局相同的方式阻塞,因此使頁面同步並將其動態讀取也推入一個 <Suspense> 包裝的葉子中。fallback={null} 僅在閘門成功時不渲染任何內容時才正確;對於資料,fallback 必須是真實的載入骨架(請參閱 D1)。

每個其他阻塞器形狀 — cookies()/headers()、未快取的 fetch 或資料庫讀取、searchParams、metadata、viewport、非確定性值(Date.now()Math.random()crypto.randomUUID())— 在遇到時都會顯示其自己的見解:建置會列印一個 https://nextjs.org/docs/messages/<slug> 連結。預設建置輸出通常被縮寫,可能沒有可用的堆疊追蹤;添加 --debug-prerender 以獲取完整的失敗框架並報告第一個之後的每個阻塞器。使用 next build --debug-build-paths "app/<route>/**" 將建置範圍限定在你正在處理的路由,而不是重建整個應用程式。打開該頁面並應用其配方;不要根據內聯訊息即興發揮。

每種形狀的 before→after 配方在 reference/patterns.md 中,它將其映射到解釋它的見解。

對於即時導航目標,這些每個錯誤頁面沒有強調的幾件事:

  • 根佈局中的邊界對於客戶端導航是不夠的。 它通過頁面載入檢查,但讓兄弟客戶端導航阻塞;將邊界放在來源和目標路由共享的最低佈局下方。
  • 將 LCP 元素(通常是主要標題)保留在任何邊界之外,以便它在殼層中繪製,而不是等待串流。
  • 綠色檢查並不總是即時的。 export const instant = false 選擇該區段退出驗證,而導航仍然阻塞,並且文件 <body> 上方的 <Suspense> 預先渲染一個空殼層 — 兩者都不會使路由即時。

D1:重用路由現有的載入 UI;不要手動建立骨架

在編寫任何骨架之前,按順序搜尋儲存庫中此路由已存在的載入 UI:

  1. 路由的 loading.tsx
  2. 與組件共置的匯出 *Skeleton
  3. 組件自身 <Suspense> 內部的 fallback。

分歧點 是來源和目標路由共享的最低佈局:軟導航僅重新渲染其下方的區段,而初始載入從根重新運行每個佈局。(也稱為共享邊界。)分歧點上方的 loading.tsx 僅填充初始載入殼層;它位於軟導航重新渲染範圍之上。目標區段的 loading.tsx 本身就是軟導航進入該區段的樹內邊界,並服務於兩者。重用實際覆蓋你正在提交的導航的任何邊界;在分歧點下方,loading.tsx 和共置骨架在該目的上可互換。

如果組件沒有骨架,則將其載入標記提取到旁邊的共置骨架中。不要編寫一個反映頁面佈局的新骨架:它複製結構,隨著頁面變化而漂移,並將設計拉回單一粗略邊界。重用組件自身的骨架也使預取殼層與載入的 UI 保持一致。

例外:如果延遲組件對某些使用者渲染 null(例如,受標誌保護的控制項),則 fallback={null} 是正確的,因為骨架會閃現然後折疊。

D2:殼層必須在每個斷點匹配真實渲染

凍結在一個斷點的骨架在其他斷點上會錯位。以相同方式修復:一個響應式組件同時渲染即時 UI 和殼層(D1 骨架在其資料槽中),因此斷點切換只發生一次。透過在兩個寬度下重新斷言殼層標記來驗證(await page.setViewportSize({ width: 1280, height: 800 }),然後 { width: 390, height: 844 }),或添加一個行動 Playwright 專案,使此閘門與其他閘門一樣可機器檢查。詳細資訊:reference/real-app-patterns.md

D-閘門:當階段 C 的鎖定測試在生產建置框架上的鎖定下通過 GREEN 時,階段 D 完成,而不是在程式碼編譯時。該 GREEN 是修復循環的確定性停止點;繼續進行 E。

當讀取無法向下推時(每個請求建立的 ID、全動態頁面、整個子樹需要的每次請求認證/範圍讀取),就沒有可擴大的殼層。不要強迫一個:將路由選擇加入 運行時預取,以便預取在點擊之前運行動態渲染,軟導航提交真實內容。請參閱 運行時預取 以了解機制(路由上的 prefetch = 'allow-runtime' 加上完整的 <Link prefetch={true}>)和 預取期間動態資料的見解 以了解採用。文件未涵蓋的 instant() 特定陷阱:

  • 完整預取是強制性的。 自動/PPR 預取在運行時生成之前退出(subtreeHasSpeculativePrefetch);只有 prefetch={true} / kind: 'full' 才能到達它。如果你設定 prefetch = 'allow-runtime' 但它仍然是 RED,則連結正在進行自動預取。
  • 所有葉子槽必須一致。 內容區段上的 allow-runtime 但兄弟 @header/@sidebar 葉子上沒有,會使路由的運行時條目不完整,因此鎖定回退到殼層。一起翻轉所有葉子。
  • 預取規範 URL。 href 為 307 重定向的連結無法預取 — 預取收到重定向,而不是樹。將連結和預取指向最終 URL。
  • 不要全面使用完整預取。 它會獲取目標的 所有 動態資料;在每個連結的懸停上發出它是一種浪費。將 kind: 'full' 範圍限定為僅運行時預取目標。
  • 標記必須是已提交的節點,而不是 RSC 位元組。 內容通常是客戶端組件,因此其文字不在預取回應中。斷言一個在客戶端子樹提交時渲染的 data-testid

E. 一致性:重構僅改變了路由是否即時

向下推是一種機械式轉換,而不是重新設計。之後,路由必須渲染與之前相同的樹、資料、順序、空狀態和錯誤狀態、重定向和互動;唯一可觀察的差異是殼層現在立即提交。驗證:

  • 相同的渲染輸出。 移動的 await 計算並返回相同的值;串流之後,路由為測試使用者顯示與基礎分支相同的內容。
  • 副作用仍然觸發。 延遲的 redirect()notFound() 仍然發生,在請求時而不是在預渲染期間。確認未經授權的使用者仍然被重定向,缺失的記錄仍然返回 404。
  • 兩個視口在串流後都到達真實 UI(D2)。
  • 客戶端狀態存活。 因為佈局 UI 被提升到穩定的殼層中,而不是在解析時被交換,所以開啟的選單、滾動位置、焦點和輸入狀態在串流期間持續存在。

如果除了路由是否即時之外的任何內容發生變化,請縮小重構範圍。

F. 差異測試

僅還原修復 → RED;重新應用 → GREEN;連結兩次運行(reference/red-test-robustness.md)。在部署的框架上,在信任其顏色之前,確認每次運行都是活的(LIVENESS,階段 A)。

G. 審查(PR 檢查清單)

如果 RED 從未可信,那麼綠色最終狀態就毫無意義。測試可信度項目是穩健性檢查清單(reference/red-test-robustness.md);確認它們,然後要求這些 PR 特定項目:

  • [ ] 顯示差異測試:沒有修復時為 RED,有修復時為 GREEN,運行已連結。
  • [ ] 確認一致性(E):相同的內容、重定向和狀態。
  • [ ] 重用現有載入 UI(D1):沒有新的頁面鏡像骨架。
  • [ ] 殼層在桌面和行動寬度下匹配真實渲染(D2)

整個工作流程的停止條件: 階段 C 的鎖定測試在框架上為 GREEN,差異測試(F)成立,並且上面的每個項目都已檢查。直到所有三個條件都成立,你才完成。

在測試中驅動導航

  • 軟導航 → 驅動真實的 <Link> 點擊。初始載入 → 在 instant() 內部使用 page.goto() 搭配 baseURL 選項。不要用 goto 替代軟導航結論;兩個殼層可能不同(test-template.mdreference/real-app-patterns.md)。
  • 對於平行路由,只有變更的槽在軟導航時重新渲染;客戶端渲染的導航 UI 根本不會重新渲染。不要追逐導航從未觸及的槽(reference/real-app-patterns.md)。

文件

  • rig-template.md:階段 0,六個問題的框架發現,instant-nav.rig.md 模板,以及填寫的範例(僅本地、通用 CI、預覽部署)。
  • test-template.md:兩種導航類型的提交 instant() 規格(階段 C),以及刪除前 PR 的基準線支架(階段 B)。
  • reference/red-test-robustness.md:C-閘門和階段 F。不可信 RED 的分類、檢查清單、差異測試配方、空洞通過失敗模式以及處理過的案例。
  • reference/real-app-patterns.md:平行路由、延遲認證閘門、初始載入與軟導航殼層、空殼層失敗模式、響應式骨架不匹配、邊緣案例。