
next-cache-components-optimizer
热门通过设置代理循环,在缓存组件/PPR下,使Next.js路由在初始加载(硬导航)和客户端导航(软导航)时实现即时导航。将目标编码为一个失败的@next/playwright instant()端到端测试,并逐步使其通过,每次验证一个路由;交付的测试随后防止回归。当被要求使路由导航即时(其静态外壳立即提交)、修复静态外壳未预渲染/服务/预取的路由、扩展路由的静态外壳或修复其首次绘制缓慢、诊断哪个Suspense边界阻止路由进入静态外壳,或为路由编写instant()端到端守卫时使用。需要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与缓存组件
工作流依赖于当前Next.js提供的框架能力:
- Next.js 16.3+ 并在
next.config.ts中启用cacheComponents: true。没有缓存组件就没有可优化的静态外壳。 @next/playwright与项目的next在同一发布线;它提供instant()。使用npm ls next @next/playwright(或项目的包管理器)验证,如果不同则对齐。匹配的测试API在next运行时中,由experimental.exposeTestingApiInProductionBuild配置标志控制(阶段A)。
如果项目不满足这些条件,先升级(npx @next/codemod upgrade 自动化大部分),然后在 next.config.ts 中启用缓存组件:
export default { cacheComponents: true }
启用标志会首先暴露阻塞路由;next-cache-components-adoption 技能驱动该采用。一旦应用在缓存组件下构建,就可以使用此优化器。
这个门是故意的:技能针对当前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登录夹具),带有其标志、计划、角色和数据。这建立了标记是真实且可达的:不是标志门控、不是重定向、不是猜测的选择器。套件作为测试账户运行,而不是作者的会话;该环境漂移(框架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和解析树中都渲染,将其提升到边界之上。
最常见的阻塞器:布局中的顶级 await 在fallback路由上
app/[locale]/(app)/[tenant]/dashboard/...
│ generateStaticParams ✅ │ 无 generateStaticParams → fallback路由
当路由中的任何动态片段缺少 generateStaticParams 时,该路由是fallback路由,并且所有参数推迟到请求时间,包括已枚举的参数。布局中的顶级 await(await params、请求时会话读取、认证门)然后阻塞整个子树离开静态外壳,即使它读取静态已知的参数。最小形状:一个动态片段路由,其中一个片段缺少 generateStaticParams,加上其上方布局中的顶级 await。
修复:推迟门,渲染子组件
无条件渲染 children;将顶级 await 移动到 <Suspense fallback={null}> 包装的子组件中。机制和before→after:reference/real-app-patterns.md,“推迟认证门”。
也修复外壳下方的页面,而不仅仅是布局。 页面级别的顶级 await(通常是 await params)以与布局相同的方式阻塞,因此使页面同步并将其动态读取推入 <Suspense> 包装的叶子中。fallback={null} 仅在门成功时渲染为空时正确;对于数据,fallback必须是真实的加载骨架(参见D1)。
每个其他阻塞器形状——cookies()/headers()、未缓存的fetch或数据库读取、searchParams、元数据、视口、非确定性值(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:并行路由、推迟认证门、初始加载与软导航外壳、空外壳失败模式、响应式骨架不匹配、边缘情况。





