
next-cache-components-optimizer
熱門透過設定代理循環,在 Cache Components / PPR 下,讓 Next.js 路由達到即時導航(初始載入(硬導航)與客戶端導航(軟導航))。將目標編碼為失敗的 @next/playwright instant() e2e 測試,並逐一驗證路由,直到測試通過;通過的測試可防止回歸。當被要求讓路由的導航變為即時(其靜態殼層立即提交)、修復靜態殼層未預先渲染/提供/預取的路由、擴大路由的靜態殼層或修復其緩慢的首次繪製、診斷哪個 Suspense 邊界阻止路由進入靜態殼層,或為某個路由編寫 instant() e2e 防護時使用。需要 Next.js 16.3+ 並啟用 cacheComponents;若版本較舊則引導升級。
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/playwrightinstant()。 此技能使用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 描述的框架。每個平台都有兩個不變項:
-
永遠不要在
next dev上測量。 它不會預取,且其鎖定對於阻塞路由不可靠,因此開發環境的instant()結果不是有效的 RED 或 GREEN。 -
被測量的建置必須暴露測試 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 路由,並且 所有 參數都延遲到請求時,包括已列舉的參數。佈局中的頂層 await(await 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:
- 路由的
loading.tsx; - 與組件共置的匯出
*Skeleton; - 組件自身
<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.md、reference/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:平行路由、延遲認證閘門、初始載入與軟導航殼層、空殼層失敗模式、響應式骨架不匹配、邊緣案例。





