next-cache-components-optimizer
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesenext-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 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 — (before→after for each blocker type) and
(parallel routes, auth gates, the empty-shell
and responsive-skeleton failure modes). Read one only when its phase points
there.
@next/playwrightinstant()reference/patterns.mdreference/real-app-patterns.md搭建一个智能优化循环,将Next.js路由从「非即时」状态转变为「即时」状态并维持该状态。该循环采用测试驱动的方式:将目标转化为一个失败的 测试,逐步修复使其通过,然后将该测试作为回归防护机制交付。针对每个目标路由运行一次该循环。按顺序完成P → G各阶段,每个阶段结束后需通过验证门限。修复方案存储在两个延迟加载的参考文档中——(每种阻塞类型的前后对比方案)和(并行路由、权限校验、空外壳和响应式骨架失效模式)。仅在对应阶段指向时才阅读相应文档。
@next/playwrightinstant()reference/patterns.mdreference/real-app-patterns.mdWhat 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. This skill usesinstant()as a ruler, not a stopwatch (phase A). It comes frominstant()(installed alongside@next/playwright, on the same release line as@playwright/test), 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.next - Yours: the rig. How you build, deploy, authenticate, configure
Playwright, and loop belongs to your stack, not to this skill. A local
, 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.
next build && next start
有一项内容是固定不变的,其余部分可根据你的需求自定义。在将以下任何命令、平台或环境变量视为要求之前,请先阅读此部分。
- 固定规则:验证循环。除非能证明静态外壳已最大化,否则优化毫无意义。证明方式是自动化检查:在锁定动态数据的情况下,静态外壳仍能正常提交。红色(RED)表示存在差距,绿色(GREEN)表示差距已消除,最终交付的测试将作为回归防护机制。测试必须在类生产环境构建中运行,且不能无意义地通过。只需搭建一次该循环,后续所有优化都可通过该结构进行验证。循环本身是交付成果,而非单个路由。
- 核心机制:
@next/playwright。本技能使用instant()作为标尺,而非计时器(阶段A)。它来自instant()(与@next/playwright一同安装,版本需与@playwright/test保持一致),因此不依赖任何宿主环境。请保留该机制。手动计时导航过于不稳定,无法信任,而这正是本技能要解决的失效模式。next - 自定义内容:测试环境。构建、部署、鉴权、配置Playwright以及循环的方式取决于你的技术栈,而非本技能。本地、CI/ staging容器、每次推送的预览部署都是有效的测试环境;判断结果来自构建本身,而非平台。阶段0会将固定规则映射到你的代码库中。请将以下所有平台名称、环境变量拼写和命令视为示例,而非硬性要求,需根据实际情况调整。
next build && next start
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 default under Partial Prefetching — re-rendering only the segments that change.
<Link>
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.mdGoal
目标
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 pass alone is satisfied by a blank
shell (the empty-shell failure mode,
).
instant()fallback={null}reference/real-app-patterns.mdinstant()The GREEN under the lock is the deterministic verdict; each gate keeps it
trustworthy.
优化的目标是最大化静态外壳:最有意义的预渲染内容立即提交,只有真正的每请求数据会在之后流式加载。交付的测试会确定性地编码「存在且即时」的状态;「非空白」是工作流通过判断强制执行的额外标准(D1/D2/E),因为仅测试通过可能是由空白的外壳导致的(空外壳失效模式,详见)。
instant()fallback={null}reference/real-app-patterns.mdinstant()锁定状态下的绿色(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 (always fresh, still instant) rather than guess a
<Suspense>.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 in
cacheComponents: true. Without Cache Components there is no static shell to optimize.next.config.ts - on the same release line as the project's
@next/playwright; it providesnext. Verify withinstant()(or the project's package manager) and align them if they differ. The matching testing API is in thenpm ls next @next/playwrightruntime, gated by thenextconfig flag (phase A).experimental.exposeTestingApiInProductionBuild
If the project does not meet these, upgrade first (
automates most of it), then enable Cache Components in :
npx @next/codemod upgradenext.config.tsts
export default { cacheComponents: true }Enabling the flag surfaces the blocking routes to resolve first; the
skill drives that adoption. Reach for this optimizer once the app builds under
Cache Components.
next-cache-components-adoptionThis 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。没有Cache Components就没有可优化的静态外壳。cacheComponents: true - ****版本需与项目中的
@next/playwright版本保持一致;它提供next方法。使用instant()(或项目的包管理器)进行验证,若版本不一致则对齐。匹配的测试API位于npm ls next @next/playwright运行时中,由next配置标志启用(阶段A)。experimental.exposeTestingApiInProductionBuild
如果项目不符合这些要求,请先升级(可自动完成大部分升级工作),然后在中启用Cache Components:
npx @next/codemod upgradenext.config.tsts
export default { cacheComponents: true }启用该标志会先显示需要解决的阻塞路由;技能可推动该适配工作。当应用在Cache Components下能正常构建后,再使用本优化器。
next-cache-components-adoption该门限是刻意设置的:本技能针对最新版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
. 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 .
instant-nav.rig.mdrig-template.mdIf the repo has no Playwright e2e harness yet, standing up a minimal one
(, a config with , one authenticated path) is part
of this step; the loop does not assume a pre-existing suite.
@next/playwrightbaseURL本技能中的原则是固定的,但运行这些原则的基础设施由你自定义。首次在代码库中使用时,探索项目的构建、部署、鉴权和测试方式(先检查代码库,仅在无法获取答案时询问用户),然后将答案写入提交的文件中。后续每次运行时读取该文件即可,无需重新探索。六个问题(BUILD / EXPOSE / RUN / TEST USER / DRIFT / LOOP)、文件模板和填充示例(仅本地、通用CI+容器、预览部署)均在****中。
instant-nav.rig.mdrig-template.md如果代码库还没有Playwright端到端测试框架,搭建一个最小化的框架(、带有的配置、一个已鉴权的路径)是本步骤的一部分;循环不假设已有测试套件。
@next/playwrightbaseURLA. RIG: a production build with the testing API exposed
A. 搭建测试环境:暴露测试API的生产环境构建
Stand up the rig described by . Two invariants hold on
every platform:
instant-nav.rig.md-
Never measure on. It does not prefetch, and its lock is unreliable for blocking routes, so a dev
next devresult is not a valid RED or GREEN.instant() -
The measured build must expose the testing API. Otherwisesilently no-ops and the test passes vacuously (see
instant()). 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 inreference/red-test-robustness.mdis the in-band guarantee. Wiretest-template.mdto a condition that is true for every build you measure and never true in production:experimental.exposeTestingApiInProductionBuildtsexperimental: { // 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
, a CI/staging container, and a preview deploy are
all equally valid; the verdict comes from the build, not the platform. See
for filled examples.
next build && next startrig-template.mdFor any deployed or remote build, poll the rig's LIVENESS probe to confirm the
artifact contains before trusting a verdict (a stale deploy reads as a
false RED or GREEN); a local needs none. The probe
mechanism is in (question 6).
HEADnext build && next startrig-template.md按照中的描述搭建测试环境。在所有平台上都需遵守两个固定规则:
instant-nav.rig.md-
切勿在环境中测试。该环境不支持预获取,且其锁定机制对于阻塞路由不可靠,因此开发环境中的
next dev结果不是有效的RED或GREEN。instant() -
被测构建必须暴露测试API。否则会静默无操作,测试会无意义地通过(详见
instant())。锁定机制生效的证明是阶段C的RED本身:未修复的目标路由是已知的阻塞路由,其在锁定状态下的RED表明锁定机制在该构建中生效(C-门限);reference/red-test-robustness.md中的自验证变体是内置的保障。将test-template.md设置为一个仅在被测构建中为true、在生产环境中永远为false的条件:experimental.exposeTestingApiInProductionBuildtsexperimental: { // 使用你的平台提供的条件,并将其记录在测试环境文件中: // 本地: 如下所示的显式启用 // 通用CI: process.env.DEPLOY_ENV === 'staging' // Vercel: process.env.VERCEL_ENV === 'preview' exposeTestingApiInProductionBuild: process.env.EXPOSE_TESTING_API === '1', }
测试环境可以是任何暴露测试API的类生产环境构建:本地、CI/staging容器、预览部署都是有效的;判断结果来自构建本身,而非平台。中有填充示例。
next build && next startrig-template.md对于任何已部署或远程构建,在信任判断结果之前,轮询测试环境的LIVENESS探针以确认工件包含最新的(陈旧的部署会导致错误的RED或GREEN);本地无需此操作。探针机制在(问题6)中。
HEADnext build && next startrig-template.mdB. BASELINE (unlocked): development scaffold, do not ship
B. 基准测试(未锁定):开发脚手架,无需交付
Drive the real navigation with no lock and assert that the
destination's 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: .
Delete this baseline before the PR.
instant()SHELL_MARKERtest-template.md在不使用锁定的情况下驱动真实导航,并断言目标路由的以测试用户身份渲染:即端到端套件鉴权使用的账户(CI环境中为CI账户;本地环境中为你的端到端登录 fixture),包含其标志、方案、角色和数据。这可确保标记真实且可访问:未被标志限制、未被重定向、不是猜测的选择器。套件以测试账户运行,而非作者的会话;环境差异(测试环境的DRIFT列表)是导致RED不可信的常见原因。脚手架和运行命令:。在PR前删除此基准测试。
instant()SHELL_MARKERtest-template.mdC. RED (locked) + the VERIFY-RED gate
C. 红色测试(锁定) + 验证红色测试门限
Wrap the same navigation in ; assert the shell commits under the
lock. A RED here is the gap. This is the test that ships
().
instant()test-template.mdPrefer 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.mdC-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 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
. Read it now.
SHELL_MARKERreference/red-test-robustness.md将相同的导航包装在中;断言外壳在锁定状态下提交。此处的RED表示存在差距。这是需要交付的测试()。
instant()test-template.md当路由有延迟加载内容时,优先使用自验证变体。如果路由在阻塞时无法构建,或者cookie/会话读取始终为GREEN,请使用中的红色测试方案。
reference/red-test-robustness.mdC-门限:在确认红色测试可信之前,不要开始优化。因错误原因导致的RED会让你去优化一个从未出现问题的路由。
解决该问题的关键:是否能在未锁定状态下以测试用户身份渲染? 请以测试用户身份重新运行阶段B来回答,而非在交付的测试中添加断言。两种分支解决方案(否→标记或环境错误;是→真实差距,继续到D)、不可信RED的完整分类、检查清单和实际案例均在****中。请立即阅读。
SHELL_MARKERreference/red-test-robustness.mdD. FIX: push each boundary down to the data it guards
D. 修复:将每个边界下移到它所防护的数据
The anti-pattern: one coarse boundary. A single high in the
tree with a page-level fallback has three costs:
<Suspense>- 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最常见的阻塞因素:回退路由的布局中存在顶层await
awaitapp/[locale]/(app)/[tenant]/dashboard/...
│ generateStaticParams ✅ │ no generateStaticParams → fallback routeWhen any dynamic segment in the route lacks , the route
is a fallback route, and all params defer to request time, including the
enumerated ones. A top-level in a layout (, 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 , plus a
top-level in the layout above it.
generateStaticParamsawaitawait paramsgenerateStaticParamsawaitapp/[locale]/(app)/[tenant]/dashboard/...
│ generateStaticParams ✅ │ 无generateStaticParams → 回退路由当路由中的任何动态片段缺少时,该路由就是回退路由,所有参数都会延迟到请求时获取,包括已枚举的参数。布局中的顶层(、请求时的会话读取、权限校验)会阻止整个子树进入静态外壳,即使它读取的是静态已知的参数。最小结构:一个动态片段路由,其中一个片段缺少,加上其上方布局中的顶层。
generateStaticParamsawaitawait paramsgenerateStaticParamsawaitThe fix: defer the gate, render children
修复方案:延迟校验,渲染子组件
Render unconditionally; move the top-level into a
-wrapped child. Mechanism and before→after:
, "Deferring an auth gate".
childrenawait<Suspense fallback={null}>reference/real-app-patterns.mdFix the page below the shell too, not only the layout. A page-level
top-level (commonly ) blocks the same way the layout's
does, so make the page sync and push its dynamic reads into a
-wrapped leaf as well. is correct only when a gate renders nothing on
success; for data, the fallback must be a real loading skeleton (see D1).
awaitawait params<Suspense>fallback={null}Every other blocker shape — /, uncached fetch or database
reads, , metadata, viewport, non-deterministic values (,
, ) — surfaces its own insight when you hit
it: the build prints a link. The
default build output is often abbreviated and may carry no usable stack trace;
add for the full failing frame and to report every blocker
past the first. Scope the build to the route you're on with
rather than rebuilding the app.
Open that page and apply its recipe; don't improvise from the inline message.
cookies()headers()searchParamsDate.now()Math.random()crypto.randomUUID()https://nextjs.org/docs/messages/<slug>--debug-prerendernext build --debug-build-paths "app/<route>/**"The before→after recipe for each shape is in , which maps it to the insight
that explains it.
reference/patterns.mdA 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. opts the segment out of validation while the navigation still blocks, and a
export const instant = falseabove the document<Suspense>prerenders an empty shell — neither makes the route instant.<body>
无条件渲染;将顶层移到被包裹的子组件中。机制和前后对比:中的「延迟权限校验」。
childrenawait<Suspense fallback={null}>reference/real-app-patterns.md同时修复外壳下方的页面,而不仅仅是布局。页面级的顶层(通常是)会像布局中的一样阻塞,因此请将页面改为同步,并将其动态读取操作移到被包裹的叶子节点中。只有当校验成功时不渲染任何内容时,才是正确的;对于数据,回退组件必须是真实的加载骨架(见D1)。
awaitawait paramsawait<Suspense>fallback={null}其他所有阻塞因素——/、未缓存的fetch或数据库读取、、元数据、视口、非确定性值(、、)——在遇到时都会给出相应的提示:构建会打印一个链接。默认的构建输出通常是缩写的,可能没有可用的堆栈跟踪;添加可获取完整的失败栈帧并报告第一个之后的所有阻塞因素。使用仅构建你正在处理的路由,而非整个应用。打开该页面并应用其方案;不要根据内联消息自行修改。
cookies()headers()searchParamsDate.now()Math.random()crypto.randomUUID()https://nextjs.org/docs/messages/<slug>--debug-prerendernext 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:
- the route's ;
loading.tsx - an exported colocated with the component;
*Skeleton - 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 above the divergence point fills only
the initial-load shell; it sits above the soft-nav re-render scope. A
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,
and colocated skeletons are interchangeable for that purpose.
loading.tsxloading.tsxloading.tsxIf 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.
See: Streaming
and loading states.
Exception: if the deferred component renders for some users (for
example, a flag-gated control), is correct, since a skeleton
would flash and then collapse.
nullfallback={null}在编写任何骨架之前,先在代码库中搜索该路由已有的加载UI,优先级如下:
- 路由的;
loading.tsx - 与组件同目录导出的组件;
*Skeleton - 组件自身中已有的回退组件。
<Suspense>
分歧点是源路由和目标路由共享的最低布局:软导航仅重新渲染该布局下方的片段,而初始加载会重新运行从根开始的所有布局(也称为共享边界)。分歧点上方的仅填充初始加载的外壳;它位于软导航重新渲染范围之上。目标片段处的本身就是软导航进入该片段的树内边界,可同时适用于两种导航方式。复用能覆盖你要交付的导航的边界;在分歧点下方,和同目录骨架在该用途上是可互换的。
loading.tsxloading.tsxloading.tsx如果组件没有骨架,将其加载标记提取到同目录的骨架组件中。不要编写镜像页面布局的全新骨架:这会重复结构,随着页面变化而逐渐不一致,并将设计拉回单一粗糙边界的模式。复用组件自身的骨架还能确保预获取的外壳与加载后的UI一致。
例外情况:如果延迟加载的组件对某些用户渲染(例如,标志 gated 的控件),则是正确的,因为骨架会闪烁然后消失。
nullfallback={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
(, then
), or by adding a mobile Playwright project, so
this gate is as machine-checkable as the others. Detail:
.
await page.setViewportSize({ width: 1280, height: 800 }){ width: 390, height: 844 }reference/real-app-patterns.mdD-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
, , 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 , and cached URL-dependent content. See
Optimizing prefetching
and pattern 10 in for the requirements, cost trade-offs,
manual prefetch caveat, and test gotchas.
paramssearchParams<Link prefetch={true}>reference/patterns.mdinstant()固定在一个断点的骨架在其他断点会错位。修复方式相同:使用一个响应式组件同时渲染实时UI和外壳(D1骨架放在其数据插槽中),这样断点切换只需进行一次。通过在两种宽度下重新断言外壳标记来验证(,然后是),或添加移动端Playwright项目,使该门限像其他门限一样可通过机器检查。详细信息:。
await page.setViewportSize({ width: 1280, height: 800 }){ width: 390, height: 844 }reference/real-app-patterns.mdD-门限:当阶段C中的锁定测试在生产环境构建测试环境中通过(GREEN)时,阶段D完成,而非代码编译完成时。该GREEN是修复循环的确定性停止信号;继续到E阶段。
当URL数据无法下移时(例如,整个页面依赖、或完整URL),可能没有有意义的静态外壳可扩展。不要强行扩展。按链接预获取可使软导航实现即时效果,但这不在本优化器循环范围内:它需要Partial Prefetching、和缓存的URL相关内容。参考优化预获取和中的模式10,了解要求、成本权衡、手动预获取注意事项和测试陷阱。
paramssearchParams<Link prefetch={true}>reference/patterns.mdinstant()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 s compute and return the same values; after the stream, the route shows the same content as the base branch for the test user.
await - Side effects still fire. A deferred or
redirect()still happens, at request time rather than during prerender. Confirm an unauthorized user is still redirected and a missing record still returns 404.notFound() - 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()仍会在请求时发生,而非预渲染期间。确认未授权用户仍会被重定向,缺失的记录仍会返回404。notFound() - 两种视口在流式加载完成后都能显示真实UI(D2)。
- 客户端状态得以保留。由于布局UI被提升到稳定的外壳中,而非在解析时替换,打开的菜单、滚动位置、焦点和输入状态会在流式加载过程中保持不变。
- 原有故障保持独立。如果路由在更改后出现错误,请在基准分支上重现。如果基准分支也存在相同故障,则是环境或数据问题,而非优化器的回归。
如果除了路由是否为即时状态之外还有其他变化,请缩小重构范围。
F. DIFFERENTIAL
F. 差异验证
Revert only the fix → RED; re-apply → GREEN; link both runs
(). On a deployed rig, confirm each run is live
(LIVENESS, phase A) before trusting its color.
reference/red-test-robustness.md仅回滚修复 → RED;重新应用修复 → GREEN;链接两次运行结果()。在已部署的测试环境中,在信任颜色之前确认每次运行都是实时的(LIVENESS,阶段A)。
reference/red-test-robustness.mdG. 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
(); confirm them, then require these
PR-specific items:
reference/red-test-robustness.md- 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从未可信,那么最终的绿色状态毫无意义。测试可信性项目在稳健性检查清单中();确认这些项目后,还需满足以下PR特定项目:
reference/red-test-robustness.md- 已展示差异:无修复时为RED,有修复时为GREEN,已链接两次运行结果。
- 已确认一致性(E):相同的内容、重定向和状态。
- 已复用现有加载UI(D1):无新的页面镜像骨架。
- 外壳在桌面和移动宽度下与真实渲染一致(D2)。
- 已移除基准测试:仅保留阶段C中的锁定测试。
整个工作流的停止条件:阶段C中的锁定测试在测试环境中为GREEN,差异验证(F)通过,且以上所有项目均已勾选。在这三个条件都满足之前,工作未完成。
Driving the navigation in tests
在测试中驱动导航
- Soft navigation → drive a real click. Initial load → use
<Link>insidepage.goto()with theinstant()option. Do not substitutebaseURLfor a soft-nav verdict; the two shells can differ (goto,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
文件
- : phase 0, the six-question rig discovery, the
rig-template.mdtemplate, and filled examples (local-only, generic CI, preview deploy).instant-nav.rig.md - : the shipped
test-template.mdspecs for both navigation types (phase C), and the delete-before-PR baseline scaffold (phase B).instant() - : 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/red-test-robustness.md - : parallel routes, deferring an auth gate, initial-load vs soft-navigation shells, the empty-shell failure mode, the responsive-skeleton mismatch, edge cases.
reference/real-app-patterns.md
- :阶段0,六个测试环境探索问题,
rig-template.md模板,以及填充示例(仅本地、通用CI、预览部署)。instant-nav.rig.md - :两种导航类型的交付
test-template.md测试用例(阶段C),以及PR前需删除的基准测试脚手架(阶段B)。instant() - :C-门限和阶段F。不可信RED的分类、检查清单、差异验证方案、无意义通过的失效模式和实际案例。
reference/red-test-robustness.md - :并行路由、延迟权限校验、初始加载与软导航外壳、空外壳失效模式、响应式骨架不匹配、边缘案例。
reference/real-app-patterns.md
After optimization
优化后
Once the target routes are instant, check whether the app has already adopted
Partial Prefetching (, or the relevant destination
still uses during an incremental rollout).
partialPrefetching: trueprefetch = 'partial'Make that check mechanically:
bash
rg -n "partialPrefetching|prefetch\s*=\s*['\"]partial['\"]" --glob 'next.config.*' --glob 'app/**' --glob 'src/app/**'If is in config, the app is globally adopted. If only
matches, treat those destination segments as adopted
during an incremental rollout and keep checking any other target routes.
partialPrefetching: trueprefetch = 'partial'- Already adopted: for any URL-data route that stopped at the limitation
above, consider a targeted 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.
<Link prefetch={true}> - Not adopted yet: recommend
. 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
next-partial-prefetching-adoptionusage, and optional per-link prefetching only where URL-specific content is worth the extra server work.<Link prefetch={true}>
当目标路由实现即时效果后,检查应用是否已采用Partial Prefetching(,或相关目标在增量发布期间仍使用)。
partialPrefetching: trueprefetch = 'partial'通过以下命令自动检查:
bash
rg -n "partialPrefetching|prefetch\s*=\s*['\"]partial['\"]" --glob 'next.config.*' --glob 'app/**' --glob 'src/app/**'如果配置中存在,则应用已全局采用。如果仅匹配到,则将这些目标片段视为增量发布期间已采用,并继续检查其他目标路由。
partialPrefetching: trueprefetch = 'partial'- 已采用:对于上述因限制而停止优化的URL数据路由,考虑在链接上添加针对性的,前提是在点击前准备好该URL特定内容的价值超过每个链接的服务器开销。在其他地方保持默认链接行为,使共享App Shell保持低成本基准。
<Link prefetch={true}> - 尚未采用:推荐使用。该技能将应用迁移到更好的预获取模型:默认预获取共享App Shell,减少可见链接的重复全预获取请求,对现有
next-partial-prefetching-adoption使用情况进行链接审计,仅在URL特定内容值得额外服务器开销时才启用按链接预获取。<Link prefetch={true}>