next-cache-components-optimizer

next-cache-components-optimizer

热门

通过设置代理循环,在缓存组件/PPR下,使Next.js路由在初始加载(硬导航)和客户端导航(软导航)时实现即时导航。将目标编码为一个失败的@next/playwright instant()端到端测试,并逐步使其通过,每次验证一个路由;交付的测试随后防止回归。当被要求使路由导航即时(其静态外壳立即提交)、修复静态外壳未预渲染/服务/预取的路由、扩展路由的静态外壳或修复其首次绘制缓慢、诊断哪个Suspense边界阻止路由进入静态外壳,或为路由编写instant()端到端守卫时使用。需要Next.js 16.3+并启用cacheComponents;如果版本较旧,则指导升级。

14万Star
3.1万Fork
更新于 2026/7/22
SKILL.md
readonly只读
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与缓存组件

工作流依赖于当前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 描述的框架。每个平台上有两个不变条件:

  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登录夹具),带有其标志、计划、角色和数据。这建立了标记是真实且可达的:不是标志门控、不是重定向、不是猜测的选择器。套件作为测试账户运行,而不是作者的会话;该环境漂移(框架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路由,并且所有参数推迟到请求时间,包括已枚举的参数。布局中的顶级 awaitawait 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:

  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:并行路由、推迟认证门、初始加载与软导航外壳、空外壳失败模式、响应式骨架不匹配、边缘情况。