next-cache-components-optimizer

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

next-cache-components-optimizer

next-cache-components-optimizer

Set up an agentic optimization loop that drives a Next.js route from "not instant" to "instant" and keeps it there. The loop is test-driven: encode the goal as a failing
@next/playwright
instant()
test, work it to green, and ship the test as the regression guard. Run it once per target route. Work the phases P → G in order; each ends in a gate. Fix recipes live in two lazily-read references —
reference/patterns.md
(before→after for each blocker type) and
reference/real-app-patterns.md
(parallel routes, auth gates, the empty-shell and responsive-skeleton failure modes). Read one only when its phase points there.
搭建一个智能优化循环,将Next.js路由从「非即时」状态转变为「即时」状态并维持该状态。该循环采用测试驱动的方式:将目标转化为一个失败的
@next/playwright
instant()
测试,逐步修复使其通过,然后将该测试作为回归防护机制交付。针对每个目标路由运行一次该循环。按顺序完成P → G各阶段,每个阶段结束后需通过验证门限。修复方案存储在两个延迟加载的参考文档中——
reference/patterns.md
(每种阻塞类型的前后对比方案)和
reference/real-app-patterns.md
(并行路由、权限校验、空外壳和响应式骨架失效模式)。仅在对应阶段指向时才阅读相应文档。

What is invariant, and what is yours

固定规则与自定义内容

One thing here is fixed. The rest is yours. Read this before treating any command, platform, or env var below as a requirement.
  • Invariant: the verification loop. Maximizing the shell is worthless unless you can prove it. The proof is an automated check: under a lock that gates dynamic data, the static shell still commits. RED shows the gap, GREEN shows it closed, the test ships as the regression guard. It must run on a production-like build and must not be able to pass vacuously. Stand the loop up once; every later optimization is then verifiable by construction. The loop is the deliverable, not any one route.
  • The mechanism:
    @next/playwright
    instant()
    .
    This skill uses
    instant()
    as a ruler, not a stopwatch (phase A). It comes from
    @next/playwright
    (installed alongside
    @playwright/test
    , on the same release line as
    next
    ), so it isn't tied to any host. Keep it. Timing a navigation by hand is too flaky to trust, and is the failure mode this skill exists to prevent.
  • Yours: the rig. How you build, deploy, authenticate, configure Playwright, and loop belongs to your stack, not to this skill. A local
    next build && next start
    , a CI/staging container, and a per-push preview deploy are equally valid rigs; the verdict comes from the build, never the platform. Phase 0 maps the invariant onto your repo. Read every platform name, env-var spelling, and command below as an example to translate, not a requirement.
有一项内容是固定不变的,其余部分可根据你的需求自定义。在将以下任何命令、平台或环境变量视为要求之前,请先阅读此部分。
  • 固定规则:验证循环。除非能证明静态外壳已最大化,否则优化毫无意义。证明方式是自动化检查:在锁定动态数据的情况下,静态外壳仍能正常提交。红色(RED)表示存在差距,绿色(GREEN)表示差距已消除,最终交付的测试将作为回归防护机制。测试必须在类生产环境构建中运行,且不能无意义地通过。只需搭建一次该循环,后续所有优化都可通过该结构进行验证。循环本身是交付成果,而非单个路由。
  • 核心机制:
    @next/playwright
    instant()
    。本技能使用
    instant()
    作为标尺,而非计时器(阶段A)。它来自
    @next/playwright
    (与
    @playwright/test
    一同安装,版本需与
    next
    保持一致),因此不依赖任何宿主环境。请保留该机制。手动计时导航过于不稳定,无法信任,而这正是本技能要解决的失效模式。
  • 自定义内容:测试环境。构建、部署、鉴权、配置Playwright以及循环的方式取决于你的技术栈,而非本技能。本地
    next build && next start
    、CI/ staging容器、每次推送的预览部署都是有效的测试环境;判断结果来自构建本身,而非平台。阶段0会将固定规则映射到你的代码库中。请将以下所有平台名称、环境变量拼写和命令视为示例,而非硬性要求,需根据实际情况调整。

Two navigations, two loading states

两种导航方式,两种加载状态

A route reaches the user two ways, and both must be instant:
  • Initial load (hard navigation) commits the route's prerendered static shell; deferred parts stream in behind their loading skeletons (Suspense fallbacks,
    loading.tsx
    ).
  • Client-side navigation (soft navigation) commits the destination's prefetched App Shell — the
    <Link>
    default under Partial Prefetching — re-rendering only the segments that change.
The fix patterns are identical for both; the test differs only in how the navigation is driven ("Driving the navigation in tests" below). The two shells can differ; guard the one you ship, both when both matter (
reference/real-app-patterns.md
).
路由有两种到达用户的方式,且都需要实现即时效果:
  • 初始加载(硬导航):提交路由的预渲染静态外壳;延迟加载的部分会在其加载骨架(Suspense回退组件、
    loading.tsx
    )之后流式加载。
  • 客户端导航(软导航):提交目标路由的预获取App Shell——这是Partial Prefetching下
    <Link>
    的默认行为——仅重新渲染发生变化的片段。
两种导航方式的修复模式相同;测试仅在驱动导航的方式上有所不同(下文的「在测试中驱动导航」部分)。两种外壳可能存在差异;请为你交付的外壳添加防护,若两者都重要则同时防护(
reference/real-app-patterns.md
)。

Goal

目标

Maximizing the static shell is the optimization objective: the most meaningful prerendered content commits immediately, and only genuinely per-request data streams in afterward. The shipped test deterministically encodes present ∧ instant; non-blank is the additional bar the workflow enforces by judgment (D1/D2/E), because an
instant()
pass alone is satisfied by a blank
fallback={null}
shell (the empty-shell failure mode,
reference/real-app-patterns.md
).
instant()
is a ruler, not a stopwatch: assert that the shell appears under the lock; do not time it. A trustworthy verdict requires a production build (phase A).
The GREEN under the lock is the deterministic verdict; each gate keeps it trustworthy.
优化的目标是最大化静态外壳:最有意义的预渲染内容立即提交,只有真正的每请求数据会在之后流式加载。交付的测试会确定性地编码「存在且即时」的状态;「非空白」是工作流通过判断强制执行的额外标准(D1/D2/E),因为仅
instant()
测试通过可能是由空白的
fallback={null}
外壳导致的(空外壳失效模式,详见
reference/real-app-patterns.md
)。
instant()
是标尺,而非计时器:断言外壳在锁定状态下出现,而非计时。可信的判断结果需要生产环境构建(阶段A)。
锁定状态下的绿色(GREEN)是确定性的判断结果;每个门限都确保结果可信。

Reporting to the user

向用户汇报

This loop is meant to run unattended, so it doesn't stop to ask between steps. Work the navigation the user named, finish it, and stop. What matters is how you word and present the results, not how often you interrupt. The mechanics below — the rig, RED, GREEN, the gates — are your scaffolding; the user never needs to hear those words.
  • Speak their language. Describe the gap and the result in terms of what the user sees: "navigating to the dashboard waited on the charts query before anything painted; now the layout and skeletons paint instantly and the charts stream in" — not RED/GREEN, the lock, or the phase letters.
  • Show, don't tell. When you report a route, drive the browser (or attach before/after screenshots) so the user watches the shell commit immediately and the data stream in, rather than reading a claim. Identical before and after means the fix did nothing — roll it back.
  • Present a run as a list of results the user can click through — one line per navigation: the route, what commits instantly, and what streams in — not a transcript of the loop.
  • Only surface a question for a genuine fork: a fix that would change behavior, a security-sensitive read, or a route that's dynamic by design (a per-link-prefetch candidate, not a shell to grow). A clean instant fix is not a fork — keep going. With no one to ask (an unattended run), don't block: take the safe default and note the assumption — for a cache-freshness choice, defer the read behind
    <Suspense>
    (always fresh, still instant) rather than guess a
    cacheLife
    .
该循环旨在无人值守运行,因此不会在步骤之间暂停询问。处理用户指定的导航,完成后停止。重要的是如何措辞和呈现结果,而非中断的频率。以下机制——测试环境、RED/GREEN、门限——是你的脚手架;用户无需知晓这些术语。
  • 使用用户的语言。用用户可见的术语描述差距和结果:「导航到仪表板时需等待图表查询完成才会渲染内容;现在布局和骨架会立即渲染,图表则流式加载」——而非RED/GREEN、锁定或阶段字母。
  • 展示而非告知。汇报路由时,驱动浏览器(或附上前后截图)让用户直观看到外壳立即提交、数据流式加载的过程,而非仅阅读文字说明。前后效果相同意味着修复无效——请回滚。
  • 将运行结果呈现为用户可点击的列表——每个导航对应一行:路由名称、立即提交的内容、流式加载的内容——而非循环的执行日志。
  • 仅在真正需要分支决策时提出问题:会改变行为的修复、涉及敏感安全的读取、或设计为动态的路由(适合按链接预获取的候选,而非扩展外壳的路由)。清晰的即时修复无需分支决策——继续执行。无人值守运行时,请勿阻塞:选择安全的默认方案并记录假设——对于缓存新鲜度的选择,将读取操作延迟到
    <Suspense>
    之后(始终保持新鲜,同时仍为即时),而非猜测
    cacheLife
    的值。

The workflow

工作流

- [ ] P  PREREQS      Next.js 16.3+ with cacheComponents: true; upgrade first → below
- [ ] 0  SETUP        once per repo: discover + write instant-nav.rig.md     → rig-template.md
- [ ] A  RIG          production build with the testing API exposed          → below
- [ ] B  BASELINE     unlocked: the marker renders for the test user         → test-template.md
- [ ] C  RED          locked instant(): the shell does not commit            → test-template.md
- [ ] C-gate          VERIFY-RED: stop until the RED is trustworthy          → reference/red-test-robustness.md
- [ ] D  FIX          push each Suspense boundary down to the data it guards → reference/patterns.md
- [ ]      D1 reuse the route's existing loading UI; do not hand-build skeletons
- [ ]      D2 the shell matches the real render at every breakpoint  → reference/real-app-patterns.md
- [ ] E  PARITY       the refactor changed only whether the route is instant
- [ ] F  DIFFERENTIAL revert only the fix → RED; re-apply → GREEN            → reference/red-test-robustness.md
- [ ] G  REVIEW       PR checklist (below)
Phases B and C build the test; only the locked test from C ships.

- [ ] P  前置要求      Next.js 16.3+ 且启用 cacheComponents: true;先升级 → 下文
- [ ] 0  初始化        每个代码库仅需一次:探索并编写 instant-nav.rig.md     → rig-template.md
- [ ] A  搭建测试环境  暴露测试API的生产环境构建          → 下文
- [ ] B  基准测试(未锁定):开发脚手架,无需交付     → test-template.md
- [ ] C  红色测试(锁定):外壳无法提交            → test-template.md
- [ ] C-门限          验证红色测试:直到红色测试可信再继续          → reference/red-test-robustness.md
- [ ] D  修复          将每个Suspense边界下移到它所防护的数据 → reference/patterns.md
- [ ]      D1 复用路由现有的加载UI;不要手动构建骨架
- [ ]      D2 外壳在所有断点下与真实渲染一致  → reference/real-app-patterns.md
- [ ] E  一致性验证    重构仅改变路由是否为即时状态
- [ ] F  差异验证      仅回滚修复 → 红色;重新应用修复 → 绿色            → reference/red-test-robustness.md
- [ ] G  审核          PR检查清单(下文)
阶段B和C用于构建测试;仅阶段C中的锁定测试需要交付。

P. PREREQUISITES: current Next.js with Cache Components

P. 前置要求:启用Cache Components的最新版Next.js

The workflow depends on framework capabilities that ship with current Next.js:
  • Next.js 16.3+ with
    cacheComponents: true
    in
    next.config.ts
    . Without Cache Components there is no static shell to optimize.
  • @next/playwright
    on the same release line as the project's
    next
    ; it provides
    instant()
    . Verify with
    npm ls next @next/playwright
    (or the project's package manager) and align them if they differ. The matching testing API is in the
    next
    runtime, gated by the
    experimental.exposeTestingApiInProductionBuild
    config flag (phase A).
If the project does not meet these, upgrade first (
npx @next/codemod upgrade
automates most of it), then enable Cache Components in
next.config.ts
:
ts
export default { cacheComponents: true }
Enabling the flag surfaces the blocking routes to resolve first; the
next-cache-components-adoption
skill drives that adoption. Reach for this optimizer once the app builds under Cache Components.
This gate is deliberate: the skill targets current Next.js, and none of the verdicts below are meaningful on older versions.
工作流依赖于最新版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:
ts
export default { cacheComponents: true }
启用该标志会先显示需要解决的阻塞路由;
next-cache-components-adoption
技能可推动该适配工作。当应用在Cache Components下能正常构建后,再使用本优化器。
该门限是刻意设置的:本技能针对最新版Next.js,在旧版本中以下所有判断结果均无意义。

0. SETUP: discover this project's rig, once per repo

0. 初始化:探索项目的测试环境,每个代码库仅需一次

The principles in this skill are fixed; the infrastructure they run on is yours. On first use in a repository, discover how the project builds, deploys, authenticates, and tests (inspect the repository first, and ask the user only what it cannot answer), then write the answers to a committed
instant-nav.rig.md
. Every later run reads that file instead of rediscovering. The six questions (BUILD / EXPOSE / RUN / TEST USER / DRIFT / LOOP), the file template, and filled examples (local-only, generic CI + container, preview deploy) are in
rig-template.md
.
If the repo has no Playwright e2e harness yet, standing up a minimal one (
@next/playwright
, a config with
baseURL
, one authenticated path) is part of this step; the loop does not assume a pre-existing suite.
本技能中的原则是固定的,但运行这些原则的基础设施由你自定义。首次在代码库中使用时,探索项目的构建、部署、鉴权和测试方式(先检查代码库,仅在无法获取答案时询问用户),然后将答案写入提交的
instant-nav.rig.md
文件中。后续每次运行时读取该文件即可,无需重新探索。六个问题(BUILD / EXPOSE / RUN / TEST USER / DRIFT / LOOP)、文件模板和填充示例(仅本地、通用CI+容器、预览部署)均在**
rig-template.md
**中。
如果代码库还没有Playwright端到端测试框架,搭建一个最小化的框架(
@next/playwright
、带有
baseURL
的配置、一个已鉴权的路径)是本步骤的一部分;循环不假设已有测试套件。

A. RIG: a production build with the testing API exposed

A. 搭建测试环境:暴露测试API的生产环境构建

Stand up the rig described by
instant-nav.rig.md
. Two invariants hold on every platform:
  1. Never measure on
    next dev
    .
    It does not prefetch, and its lock is unreliable for blocking routes, so a dev
    instant()
    result is not a valid RED or GREEN.
  2. The measured build must expose the testing API. Otherwise
    instant()
    silently no-ops and the test passes vacuously (see
    reference/red-test-robustness.md
    ). The lock-engagement proof is the phase-C RED itself: the unfixed target route is the known-blocking route, and its RED under the lock shows the lock engages on this build (C-gate); the self-validating variant in
    test-template.md
    is the in-band guarantee. Wire
    experimental.exposeTestingApiInProductionBuild
    to a condition that is true for every build you measure and never true in production:
    ts
    experimental: {
      // Use the condition your platform provides, and record it in the rig file:
      //   local:       an explicit opt-in, as below
      //   generic CI:  process.env.DEPLOY_ENV === 'staging'
      //   Vercel:      process.env.VERCEL_ENV === 'preview'
      exposeTestingApiInProductionBuild:
        process.env.EXPOSE_TESTING_API === '1',
    }
The rig is any production-like build that exposes the testing API: a local
next build && next start
, a CI/staging container, and a preview deploy are all equally valid; the verdict comes from the build, not the platform. See
rig-template.md
for filled examples.
For any deployed or remote build, poll the rig's LIVENESS probe to confirm the artifact contains
HEAD
before trusting a verdict (a stale deploy reads as a false RED or GREEN); a local
next build && next start
needs none. The probe mechanism is in
rig-template.md
(question 6).
按照
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
    设置为一个仅在被测构建中为true、在生产环境中永远为false的条件:
    ts
    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/staging容器、预览部署都是有效的;判断结果来自构建本身,而非平台。
rig-template.md
中有填充示例。
对于任何已部署或远程构建,在信任判断结果之前,轮询测试环境的LIVENESS探针以确认工件包含最新的
HEAD
(陈旧的部署会导致错误的RED或GREEN);本地
next build && next start
无需此操作。探针机制在
rig-template.md
(问题6)中。

B. BASELINE (unlocked): development scaffold, do not ship

B. 基准测试(未锁定):开发脚手架,无需交付

Drive the real navigation with no
instant()
lock and assert that the destination's
SHELL_MARKER
renders as the test user: the account the e2e suite authenticates as (in CI, the CI account; locally, your e2e login fixture), with its flags, plan, role, and data. This establishes that the marker is real and reachable: not flag-gated, not redirected away, not a guessed selector. The suite runs as the test account, not the author's session; that environment drift (the rig DRIFT list) is a common source of untrustworthy REDs. Scaffold and run command:
test-template.md
. Delete this baseline before the PR.
在不使用
instant()
锁定的情况下驱动真实导航,并断言目标路由的
SHELL_MARKER
测试用户身份渲染:即端到端套件鉴权使用的账户(CI环境中为CI账户;本地环境中为你的端到端登录 fixture),包含其标志、方案、角色和数据。这可确保标记真实且可访问:未被标志限制、未被重定向、不是猜测的选择器。套件以测试账户运行,而非作者的会话;环境差异(测试环境的DRIFT列表)是导致RED不可信的常见原因。脚手架和运行命令:
test-template.md
在PR前删除此基准测试

C. RED (locked) + the VERIFY-RED gate

C. 红色测试(锁定) + 验证红色测试门限

Wrap the same navigation in
instant()
; assert the shell commits under the lock. A RED here is the gap. This is the test that ships (
test-template.md
).
Prefer the self-validating variant when the route has deferred content. If the route cannot build while blocked, or a cookie/session read stays GREEN, use the RED recipes in
reference/red-test-robustness.md
.
C-gate: do not start optimizing until the RED is verified trustworthy. A RED that is red for the wrong reason sends you optimizing a route that was never broken.
The question that settles it: does
SHELL_MARKER
render without the lock, as the test user?
Answer it by re-running phase B as the test user, not by adding assertions to the shipped test. The two-branch resolution (No → marker or environment bug; Yes → genuine gap, proceed to D), the full taxonomy of untrustworthy REDs, the checklist, and worked cases are in
reference/red-test-robustness.md
. Read it now.

将相同的导航包装在
instant()
中;断言外壳在锁定状态下提交。此处的RED表示存在差距。这是需要交付的测试
test-template.md
)。
当路由有延迟加载内容时,优先使用自验证变体。如果路由在阻塞时无法构建,或者cookie/会话读取始终为GREEN,请使用
reference/red-test-robustness.md
中的红色测试方案。
C-门限:在确认红色测试可信之前,不要开始优化。因错误原因导致的RED会让你去优化一个从未出现问题的路由。
解决该问题的关键:
SHELL_MARKER
是否能在未锁定状态下以测试用户身份渲染?
请以测试用户身份重新运行阶段B来回答,而非在交付的测试中添加断言。两种分支解决方案(否→标记或环境错误;是→真实差距,继续到D)、不可信RED的完整分类、检查清单和实际案例均在**
reference/red-test-robustness.md
**中。请立即阅读。

D. FIX: push each boundary down to the data it guards

D. 修复:将每个边界下移到它所防护的数据

The anti-pattern: one coarse boundary. A single
<Suspense>
high in the tree with a page-level fallback has three costs:
  • The layout UI stays out of the static shell: only a throwaway copy of it is prerendered.
  • The entire subtree is replaced when the boundary resolves, which discards client state and shifts layout.
  • The hand-built fallback drifts out of sync as the UI changes, because it duplicates structure that also exists in the resolved tree.
The fix: hoist the static, push the Suspense down. Render the layout UI once, synchronously, in the shell, and wrap each await in a boundary scoped to the single read it guards. Only that leaf streams; the stable ancestors are reused as-is.
Rule: if an element renders in both the fallback and the resolved tree, hoist it above the boundary.
反模式:单一粗糙边界。在树的高层使用单个
<Suspense>
并带有页面级回退组件会带来三个问题:
  • 布局UI无法进入静态外壳:仅预渲染一个临时副本。
  • 边界解析时整个子树会被替换,这会丢弃客户端状态并导致布局偏移。
  • 手动构建的回退组件会随着UI变化而逐渐不一致,因为它复制了已存在于解析树中的结构。
修复方案:提升静态内容,下移Suspense。在外壳中同步渲染一次布局UI,并将每个await包装在一个仅防护单个读取操作的边界中。只有该叶子节点会流式加载;稳定的祖先节点会原样复用。
规则:如果一个元素在回退组件和解析树中都有渲染,将其提升到边界之上。

The most common blocker: a top-level
await
in a layout on a fallback route

最常见的阻塞因素:回退路由的布局中存在顶层
await

app/[locale]/(app)/[tenant]/dashboard/...
       │ generateStaticParams ✅   │ no generateStaticParams → fallback route
When any dynamic segment in the route lacks
generateStaticParams
, the route is a fallback route, and all params defer to request time, including the enumerated ones. A top-level
await
in a layout (
await params
, a request-time session read, an auth gate) then blocks the whole subtree out of the static shell, even when it reads a statically known param. Minimal shape: a dynamic-segment route with one segment lacking
generateStaticParams
, plus a top-level
await
in the layout above it.
app/[locale]/(app)/[tenant]/dashboard/...
       │ generateStaticParams ✅   │ 无generateStaticParams → 回退路由
当路由中的任何动态片段缺少
generateStaticParams
时,该路由就是回退路由,所有参数都会延迟到请求时获取,包括已枚举的参数。布局中的顶层
await
await params
、请求时的会话读取、权限校验)会阻止整个子树进入静态外壳,即使它读取的是静态已知的参数。最小结构:一个动态片段路由,其中一个片段缺少
generateStaticParams
,加上其上方布局中的顶层
await

The fix: defer the gate, render children

修复方案:延迟校验,渲染子组件

Render
children
unconditionally; move the top-level
await
into a
<Suspense fallback={null}>
-wrapped child. Mechanism and before→after:
reference/real-app-patterns.md
, "Deferring an auth gate".
Fix the page below the shell too, not only the layout. A page-level top-level
await
(commonly
await params
) blocks the same way the layout's does, so make the page sync and push its dynamic reads into a
<Suspense>
-wrapped leaf as well.
fallback={null}
is correct only when a gate renders nothing on success; for data, the fallback must be a real loading skeleton (see D1).
Every other blocker shape —
cookies()
/
headers()
, uncached fetch or database reads,
searchParams
, metadata, viewport, non-deterministic values (
Date.now()
,
Math.random()
,
crypto.randomUUID()
) — surfaces its own insight when you hit it: the build prints a
https://nextjs.org/docs/messages/<slug>
link. The default build output is often abbreviated and may carry no usable stack trace; add
--debug-prerender
for the full failing frame and to report every blocker past the first. Scope the build to the route you're on with
next build --debug-build-paths "app/<route>/**"
rather than rebuilding the app. Open that page and apply its recipe; don't improvise from the inline message.
The before→after recipe for each shape is in
reference/patterns.md
, which maps it to the insight that explains it.
A few things those per-error pages don't stress for the instant-navigation goal:
  • A boundary in the root layout isn't enough for client navigations. It passes a page-load check but leaves sibling client navigations blocking; put the boundary below the lowest layout the source and destination routes share.
  • Keep the LCP element (usually the main heading) out of any boundary, so it paints in the shell instead of waiting on a stream.
  • A green check isn't always instant.
    export const instant = false
    opts the segment out of validation while the navigation still blocks, and a
    <Suspense>
    above the document
    <body>
    prerenders an empty shell — neither makes the route instant.
无条件渲染
children
;将顶层
await
移到被
<Suspense fallback={null}>
包裹的子组件中。机制和前后对比:
reference/real-app-patterns.md
中的「延迟权限校验」。
同时修复外壳下方的页面,而不仅仅是布局。页面级的顶层
await
(通常是
await params
)会像布局中的
await
一样阻塞,因此请将页面改为同步,并将其动态读取操作移到被
<Suspense>
包裹的叶子节点中。只有当校验成功时不渲染任何内容时,
fallback={null}
才是正确的;对于数据,回退组件必须是真实的加载骨架(见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>/**"
仅构建你正在处理的路由,而非整个应用。打开该页面并应用其方案;不要根据内联消息自行修改。
每种阻塞因素的前后对比方案都在
reference/patterns.md
中,该文档还会解释其背后的原理。
针对即时导航目标,这些错误页面没有强调的几点:
  • 根布局中的边界不足以支持客户端导航。它能通过页面加载检查,但会导致兄弟客户端导航阻塞;请将边界移到源路由和目标路由共享的最低布局下方。
  • 保留LCP元素(通常是主标题),不要将其放入任何边界中,使其能在外壳中渲染,而非等待流式加载。
  • 绿色检查并不总是意味着即时
    export const instant = false
    会让片段退出验证,但导航仍会阻塞;
    <Suspense>
    放在文档
    <body>
    上方会预渲染空外壳——这两种情况都无法让路由实现即时效果。

D1: reuse the route's existing loading UI; do not hand-build skeletons

D1:复用路由现有的加载UI;不要手动构建骨架

Before writing any skeleton, search the repository for the loading UI that already exists for this route, in order:
  1. the route's
    loading.tsx
    ;
  2. an exported
    *Skeleton
    colocated with the component;
  3. the fallback already inside the component's own
    <Suspense>
    .
The divergence point is the lowest layout shared by the source and destination routes: a soft navigation re-renders only the segments below it, while an initial load re-runs every layout from the root. (Also called the shared boundary.) A
loading.tsx
above the divergence point fills only the initial-load shell; it sits above the soft-nav re-render scope. A
loading.tsx
at the destination segment is itself the in-tree boundary for a soft navigation into that segment and serves both. Reuse whichever boundary actually covers the navigation you are shipping; below the divergence point,
loading.tsx
and colocated skeletons are interchangeable for that purpose.
If a component has no skeleton, extract its loading markup into a colocated skeleton beside it. Do not author a fresh skeleton that mirrors the page layout: it duplicates structure, drifts as the page changes, and pulls the design back toward a single coarse boundary. Reusing the component's own skeleton also keeps the prefetched shell consistent with the loaded UI.
Exception: if the deferred component renders
null
for some users (for example, a flag-gated control),
fallback={null}
is correct, since a skeleton would flash and then collapse.
在编写任何骨架之前,先在代码库中搜索该路由已有的加载UI,优先级如下:
  1. 路由的
    loading.tsx
  2. 与组件同目录导出的
    *Skeleton
    组件;
  3. 组件自身
    <Suspense>
    中已有的回退组件。
分歧点是源路由和目标路由共享的最低布局:软导航仅重新渲染该布局下方的片段,而初始加载会重新运行从根开始的所有布局(也称为共享边界)。分歧点上方的
loading.tsx
仅填充初始加载的外壳;它位于软导航重新渲染范围之上。目标片段处的
loading.tsx
本身就是软导航进入该片段的树内边界,可同时适用于两种导航方式。复用能覆盖你要交付的导航的边界;在分歧点下方,
loading.tsx
和同目录骨架在该用途上是可互换的。
如果组件没有骨架,将其加载标记提取到同目录的骨架组件中。不要编写镜像页面布局的全新骨架:这会重复结构,随着页面变化而逐渐不一致,并将设计拉回单一粗糙边界的模式。复用组件自身的骨架还能确保预获取的外壳与加载后的UI一致。
例外情况:如果延迟加载的组件对某些用户渲染
null
(例如,标志 gated 的控件),则
fallback={null}
是正确的,因为骨架会闪烁然后消失。

D2: the shell must match the real render at every breakpoint

D2:外壳必须在所有断点下与真实渲染一致

A skeleton frozen to one breakpoint misaligns on the others. Fix it the same way: one responsive component renders both the live UI and the shell (D1 skeleton in its data slots), so the breakpoint switch happens once. Verify by re-asserting the shell marker at two widths (
await page.setViewportSize({ width: 1280, height: 800 })
, then
{ width: 390, height: 844 }
), or by adding a mobile Playwright project, so this gate is as machine-checkable as the others. Detail:
reference/real-app-patterns.md
.
D-gate: phase D is complete when the locked test from phase C passes GREEN under the lock on the production-build rig, not when the code compiles. That GREEN is the deterministic stop for the fix loop; proceed to E.
When URL data can't be pushed down (for example, the whole page depends on
params
,
searchParams
, or the full URL), there may be no meaningful static shell to grow. Don't force one. Per-link prefetching can make the soft navigation instant, but it is outside this optimizer loop: it requires Partial Prefetching, a
<Link prefetch={true}>
, and cached URL-dependent content. See Optimizing prefetching and pattern 10 in
reference/patterns.md
for the requirements, cost trade-offs, manual prefetch caveat, and
instant()
test gotchas.
固定在一个断点的骨架在其他断点会错位。修复方式相同:使用一个响应式组件同时渲染实时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阶段。
当URL数据无法下移时(例如,整个页面依赖
params
searchParams
或完整URL),可能没有有意义的静态外壳可扩展。不要强行扩展。按链接预获取可使软导航实现即时效果,但这不在本优化器循环范围内:它需要Partial Prefetching、
<Link prefetch={true}>
和缓存的URL相关内容。参考优化预获取
reference/patterns.md
中的模式10,了解要求、成本权衡、手动预获取注意事项和
instant()
测试陷阱。

E. PARITY: the refactor changed only whether the route is instant

E. 一致性验证:重构仅改变路由是否为即时状态

The push-down is a mechanical transform, not a redesign. Afterward the route must render the same tree, data, ordering, empty and error states, redirects, and interactions as before; the only observable difference is that the shell now commits instantly. Verify:
  • Same render output. The moved
    await
    s compute and return the same values; after the stream, the route shows the same content as the base branch for the test user.
  • Side effects still fire. A deferred
    redirect()
    or
    notFound()
    still happens, at request time rather than during prerender. Confirm an unauthorized user is still redirected and a missing record still returns 404.
  • Both viewports reach the real UI after the stream (D2).
  • Client state survives. Because the layout UI is hoisted into the stable shell rather than swapped on resolve, open menus, scroll position, focus, and input state persist across the stream.
  • Pre-existing failures stay separate. If the route errors after the change, reproduce it on the base branch. The same failure there is an environment or data problem, not an optimizer regression.
If anything other than whether the route is instant changed, reduce the refactor.
下移操作是机械转换,而非重新设计。之后路由必须渲染与之前相同的树、数据、顺序、空状态和错误状态、重定向及交互;唯一可观察到的差异是外壳现在会立即提交。验证以下内容:
  • 相同的渲染输出。移动后的
    await
    计算并返回相同的值;流式加载完成后,路由为测试用户显示的内容与基准分支相同。
  • 副作用仍会触发。延迟的
    redirect()
    notFound()
    仍会在请求时发生,而非预渲染期间。确认未授权用户仍会被重定向,缺失的记录仍会返回404。
  • 两种视口在流式加载完成后都能显示真实UI(D2)。
  • 客户端状态得以保留。由于布局UI被提升到稳定的外壳中,而非在解析时替换,打开的菜单、滚动位置、焦点和输入状态会在流式加载过程中保持不变。
  • 原有故障保持独立。如果路由在更改后出现错误,请在基准分支上重现。如果基准分支也存在相同故障,则是环境或数据问题,而非优化器的回归。
如果除了路由是否为即时状态之外还有其他变化,请缩小重构范围。

F. DIFFERENTIAL

F. 差异验证

Revert only the fix → RED; re-apply → GREEN; link both runs (
reference/red-test-robustness.md
). On a deployed rig, confirm each run is live (LIVENESS, phase A) before trusting its color.
仅回滚修复 → RED;重新应用修复 → GREEN;链接两次运行结果(
reference/red-test-robustness.md
)。在已部署的测试环境中,在信任颜色之前确认每次运行都是实时的(LIVENESS,阶段A)。

G. REVIEW (PR checklist)

G. 审核(PR检查清单)

A green final state means nothing if the RED was never trustworthy. The test-trustworthiness items are the robustness checklist (
reference/red-test-robustness.md
); confirm them, then require these PR-specific items:
  • Differential shown: RED without the fix, GREEN with it, runs linked.
  • Parity confirmed (E): same content, redirects, and state.
  • Existing loading UI reused (D1): no new page-mirroring skeleton.
  • Shell matches the real render at desktop and mobile widths (D2).
  • Baseline removed: only the locked test from C remains.
Stop condition for the whole workflow: the locked test from C is GREEN on the rig, the differential (F) holds, and every item above is checked. Until all three hold, you are not done.
如果RED从未可信,那么最终的绿色状态毫无意义。测试可信性项目在稳健性检查清单中(
reference/red-test-robustness.md
);确认这些项目后,还需满足以下PR特定项目:
  • 已展示差异:无修复时为RED,有修复时为GREEN,已链接两次运行结果。
  • 已确认一致性(E):相同的内容、重定向和状态。
  • 已复用现有加载UI(D1):无新的页面镜像骨架。
  • 外壳在桌面和移动宽度下与真实渲染一致(D2)
  • 已移除基准测试:仅保留阶段C中的锁定测试。
整个工作流的停止条件:阶段C中的锁定测试在测试环境中为GREEN,差异验证(F)通过,且以上所有项目均已勾选。在这三个条件都满足之前,工作未完成。

Driving the navigation in tests

在测试中驱动导航

  • Soft navigation → drive a real
    <Link>
    click. Initial load → use
    page.goto()
    inside
    instant()
    with the
    baseURL
    option. Do not substitute
    goto
    for a soft-nav verdict; the two shells can differ (
    test-template.md
    ,
    reference/real-app-patterns.md
    ).
  • With parallel routes, only the slots that change re-render on a soft navigation; client-rendered navigation UI does not re-render at all. Do not chase a slot the navigation never touches (
    reference/real-app-patterns.md
    ).
  • 软导航 → 驱动真实的
    <Link>
    点击。初始加载 → 在
    instant()
    中使用
    page.goto()
    并带上
    baseURL
    选项。不要用
    goto
    替代软导航判断;两种外壳可能存在差异(
    test-template.md
    reference/real-app-patterns.md
    )。
  • 使用并行路由时,软导航仅重新渲染发生变化的插槽;客户端渲染的导航UI根本不会重新渲染。不要去处理导航从未触及的插槽(
    reference/real-app-patterns.md
    )。

Files

文件

  • rig-template.md
    : phase 0, the six-question rig discovery, the
    instant-nav.rig.md
    template, and filled examples (local-only, generic CI, preview deploy).
  • test-template.md
    : the shipped
    instant()
    specs for both navigation types (phase C), and the delete-before-PR baseline scaffold (phase B).
  • reference/red-test-robustness.md
    : the C-gate and phase F. The taxonomy of untrustworthy REDs, the checklist, the differential recipe, the vacuous-pass failure mode, and worked cases.
  • reference/real-app-patterns.md
    : parallel routes, deferring an auth gate, initial-load vs soft-navigation shells, the empty-shell failure mode, the responsive-skeleton mismatch, edge cases.
  • 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
    :并行路由、延迟权限校验、初始加载与软导航外壳、空外壳失效模式、响应式骨架不匹配、边缘案例。

After optimization

优化后

Once the target routes are instant, check whether the app has already adopted Partial Prefetching (
partialPrefetching: true
, or the relevant destination still uses
prefetch = 'partial'
during an incremental rollout).
Make that check mechanically:
bash
rg -n "partialPrefetching|prefetch\s*=\s*['\"]partial['\"]" --glob 'next.config.*' --glob 'app/**' --glob 'src/app/**'
If
partialPrefetching: true
is in config, the app is globally adopted. If only
prefetch = 'partial'
matches, treat those destination segments as adopted during an incremental rollout and keep checking any other target routes.
  • Already adopted: for any URL-data route that stopped at the limitation above, consider a targeted
    <Link prefetch={true}>
    on the links where having that URL-specific content ready before the click is worth the per-link server work. Keep the default link behavior everywhere else so the shared App Shell remains the low-cost baseline.
  • Not adopted yet: recommend
    next-partial-prefetching-adoption
    . That skill moves the app onto the better prefetching model: shared App Shell prefetches by default, fewer duplicated full-prefetch requests for visible links, a link audit for existing
    <Link prefetch={true}>
    usage, and optional per-link prefetching only where URL-specific content is worth the extra server work.
当目标路由实现即时效果后,检查应用是否已采用Partial Prefetching(
partialPrefetching: true
,或相关目标在增量发布期间仍使用
prefetch = 'partial'
)。
通过以下命令自动检查:
bash
rg -n "partialPrefetching|prefetch\s*=\s*['\"]partial['\"]" --glob 'next.config.*' --glob 'app/**' --glob 'src/app/**'
如果配置中存在
partialPrefetching: true
,则应用已全局采用。如果仅匹配到
prefetch = 'partial'
,则将这些目标片段视为增量发布期间已采用,并继续检查其他目标路由。
  • 已采用:对于上述因限制而停止优化的URL数据路由,考虑在链接上添加针对性的
    <Link prefetch={true}>
    ,前提是在点击前准备好该URL特定内容的价值超过每个链接的服务器开销。在其他地方保持默认链接行为,使共享App Shell保持低成本基准。
  • 尚未采用:推荐使用
    next-partial-prefetching-adoption
    。该技能将应用迁移到更好的预获取模型:默认预获取共享App Shell,减少可见链接的重复全预获取请求,对现有
    <Link prefetch={true}>
    使用情况进行链接审计,仅在URL特定内容值得额外服务器开销时才启用按链接预获取。