---
title: "vercel/next.js"
description: "The React Framework"
source: https://github.com/vercel/next.js
ref: canary
license: MIT
licenseName: "MIT License"
canonical: https://skillsdocs.com/vercel/next.js
base: https://github.com/vercel/next.js/blob/canary/
provenance: mixed
chapters: 24
inlined: 24
withheld: 0
words: 24498
updated: 2026-08-28T15:36:00Z
generator: "Skills Docs"
---

> **vercel/next.js** — every Agent Skill in this repository, inlined verbatim.
>
> Canonical HTML: https://skillsdocs.com/vercel/next.js
> Per-skill Markdown: https://skillsdocs.com/vercel/next.js/<skill>.md
> Machine manifest: https://skillsdocs.com/vercel/next.js/.well-known/agent-skills/index.json
> JSON: https://skillsdocs.com/api/v1/books/vercel/next.js
> Install: `npx skills add vercel/next.js`
> Upstream: https://github.com/vercel/next.js @ `canary`
> Licence: MIT
>
> Content is mirrored from GitHub and © its authors, served unmodified. Takedown: https://github.com/DreambaseAI/skillsdocs/issues/new?labels=takedown&title=Takedown+request

# vercel/next.js

The React Framework

- **Skills:** 24
- **Authorship:** mixed — 6 of 24 are credited — skills in use here, not published from here
- **Inlined:** 24 (licence detected)
- **Words:** 24,498
- **Reading time:** 112 min
- **Stars:** 141,971

## Table of contents

1. [authoring-skills](https://skillsdocs.com/vercel/next.js/authoring-skills.md) — How to create and maintain agent skills in .agents/skills/. Use when creating a new SKILL.md, writing skill descriptions, choosing frontmatter fields, or decid…
2. [backport-pr](https://skillsdocs.com/vercel/next.js/backport-pr.md) — Backport a merged Next.js pull request from canary to a previous release branch such as next-16-2. Use when the user asks to backport, cherry-pick, or open a b…
3. [create-pr](https://skillsdocs.com/vercel/next.js/create-pr.md) — Create Git branches, commits, pushes, and GitHub pull requests for Next.js. Use when the user asks to create a branch, commit current changes, open a PR or dra…
4. [dce-edge](https://skillsdocs.com/vercel/next.js/dce-edge.md) — DCE-safe require() patterns and edge runtime constraints. Use when writing conditional require() calls, guarding Node-only imports (node:stream etc.), or editi…
5. [flags](https://skillsdocs.com/vercel/next.js/flags.md) — How to add or modify Next.js experimental feature flags end-to-end. Use when editing config-shared.ts, config-schema.ts, define-env-plugin.ts, next-server.ts,…
6. [gh-stack](https://skillsdocs.com/vercel/next.js/gh-stack.md) — Manages stacked PRs and splits multi-part work into reviewable branches with gh-stack. Use for stack creation, viewing, edits, push, submit, sync, rebase, merg…
7. [pr-status-triage](https://skillsdocs.com/vercel/next.js/pr-status-triage.md) — Triage CI failures and PR review comments using scripts/pr-status.js. Use when investigating failing CI jobs, flaky tests, or PR review feedback. Covers blocke…
8. [react-vendoring](https://skillsdocs.com/vercel/next.js/react-vendoring.md) — React vendoring and react-server layer boundaries. Use when editing entry-base.ts, $$compiled.internal.d.ts, compiled/react* packages, or taskfile.js copy_vend…
9. [router-act](https://skillsdocs.com/vercel/next.js/router-act.md) — How to write end-to-end tests using createRouterAct and LinkAccordion. Use when writing or modifying tests that need to control the timing of internal Next.js…
10. [runtime-debug](https://skillsdocs.com/vercel/next.js/runtime-debug.md) — Debug and verification workflow for runtime-bundle and module-resolution regressions. Use when diagnosing unexpected module inclusions, bundle size regressions…
11. [update-docs](https://skillsdocs.com/vercel/next.js/update-docs.md) — This skill should be used when the user asks to "update documentation for my changes", "check docs for this PR", "what docs need updating", "sync docs with cod…
12. [v8-jit](https://skillsdocs.com/vercel/next.js/v8-jit.md) — V8 JIT optimization patterns for writing high-performance JavaScript in Next.js server internals. Use when writing or reviewing hot-path code in app-render, st…
13. [write-api-reference](https://skillsdocs.com/vercel/next.js/write-api-reference.md) — Produces API reference documentation for Next.js APIs: functions, components, file conventions, directives, and config options. **Auto-activation:** User asks…
14. [write-guide](https://skillsdocs.com/vercel/next.js/write-guide.md) — Generates technical guides that teach real-world use cases through progressive examples. **Auto-activation:** User asks to write, create, or draft a guide or t…
15. [next-cache-components-adoption](https://skillsdocs.com/vercel/next.js/next-cache-components-adoption.md) — Turn on Cache Components in a Next.js app and resolve the blocking routes it surfaces. Use when the user wants to enable, adopt, or migrate to Cache Components…
16. [next-cache-components-optimizer](https://skillsdocs.com/vercel/next.js/next-cache-components-optimizer.md) — 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 navi…
17. [next-dev-loop](https://skillsdocs.com/vercel/next.js/next-dev-loop.md) — Verify Next.js runtime behavior after editing app code. Use this skill to confirm a change actually works in a running app — not just that it compiles or type-…
18. [next-partial-prefetching-adoption](https://skillsdocs.com/vercel/next.js/next-partial-prefetching-adoption.md) — Turn on Partial Prefetching in a Next.js app and work through the insights it surfaces. Use when the user wants to enable or adopt Partial Prefetching, flip th…
19. [deploy-release-test](https://skillsdocs.com/vercel/next.js/deploy-release-test.md) — Validate a commit-specific Next.js preview package and manually trigger the entire Next.js deployment test suite through the test_e2e_deploy_release.yml GitHub…
20. [gate-tests](https://skillsdocs.com/vercel/next.js/gate-tests.md) — How to use the `@gate` / `@force-gate` test directives instead of `it.skip` or fake-green skip patterns. Use when a test is known-failing under some test-matri…
21. [insight-error-page](https://skillsdocs.com/vercel/next.js/insight-error-page.md) — Write or audit an insight-kind error page for the Next.js dev overlay. Use when creating a new `errors/<slug>.mdx` page, auditing an existing one, or checking…
22. [next-rspack](https://skillsdocs.com/vercel/next.js/next-rspack.md) — Maintain @next/rspack-core and @next/rspack-binding packages. Use when editing rspack/package.json, rspack/crates/binding/Cargo.toml, rspack/rust-toolchain.tom…
23. [react-sync](https://skillsdocs.com/vercel/next.js/react-sync.md) — Build local React changes in the bundle variants consumed by Next.js, sync them into a local Next.js checkout, and test the resulting integration. Use when wor…
24. [sandbox-bench](https://skillsdocs.com/vercel/next.js/sandbox-bench.md) — Benchmark React or Next.js changes on Vercel Sandbox VMs with paired A/B statistics: react PR/commit vs base, or Next.js PR/commit vs base, measured end-to-end…


## Front matter

_The repository README, verbatim except that relative links are resolved against https://github.com/vercel/next.js/blob/canary/._

packages/next/README.md

---

## Part: Skills

---

<!-- chapter:begin slug=authoring-skills position=1 -->

## 1. authoring-skills

- **Source:** https://github.com/vercel/next.js/blob/canary/.agents/skills/authoring-skills/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/authoring-skills/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/authoring-skills.md
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: authoring-skills
description: >
  How to create and maintain agent skills in .agents/skills/. Use when
  creating a new SKILL.md, writing skill descriptions, choosing frontmatter
  fields, or deciding what content belongs in a skill vs AGENTS.md.
  Covers the supported spec fields, description writing, naming conventions,
  and the relationship between always-loaded AGENTS.md and on-demand skills.
user-invocable: false
metadata:
  internal: true
---

# Authoring Skills

Use this skill when creating or modifying agent skills in `.agents/skills/`.

## When to Create a Skill

Create a skill when content is:

- Too detailed for AGENTS.md (code templates, multi-step workflows, diagnostic procedures)
- Only relevant for specific tasks (not needed every session)
- Self-contained enough to load independently

Keep in AGENTS.md instead when:

- It's a one-liner rule or guardrail every session needs
- It's a general-purpose gotcha any agent could hit

## File Structure

```
.agents/skills/
└── my-skill/
    ├── SKILL.md          # Required: frontmatter + content
    ├── workflow.md        # Optional: supplementary detail
    └── examples.md        # Optional: referenced from SKILL.md
```

## Supported Frontmatter Fields

```yaml
---
name: my-skill # Required. Used for $name references and /name commands.
description: > # Required. How Claude decides to auto-load the skill.
  What this covers and when to use it. Include file names and keywords.
argument-hint: '<pr-number>' # Optional. Hint for expected arguments.
user-invocable: false # Optional. Set false to hide from / menu.
disable-model-invocation: true # Optional. Set true to prevent auto-triggering.
allowed-tools: [Bash, Read] # Optional. Tools allowed without permission.
model: opus # Optional. Model override.
context: fork # Optional. Isolated subagent execution.
agent: Explore # Optional. Subagent type (with context: fork).
---
```

Only use fields from this list. Unknown fields are silently ignored.

## Writing Descriptions

The `description` is the primary matching surface for auto-activation. Include:

1. **What the skill covers** (topic)
2. **When to use it** (trigger scenario)
3. **Key file names** the skill references (e.g. `config-shared.ts`)
4. **Keywords** a user or agent might mention (e.g. "feature flag", "DCE")

```yaml
# Too vague - won't auto-trigger reliably
description: Helps with flags.

# Good - specific files and concepts for matching
description: >
  How to add or modify Next.js experimental feature flags end-to-end.
  Use when editing config-shared.ts, config-schema.ts, define-env-plugin.ts.
```

## Content Conventions

### Structure for Action

Skills should tell the agent what to **do**, not just what to **know**:

- Lead with "Use this skill when..."
- Include step-by-step procedures
- Add code templates ready to adapt
- End with verification commands
- Cross-reference related skills in a "Related Skills" section

### Relationship to AGENTS.md

| AGENTS.md (always loaded)               | Skills (on demand)                                                     |
| --------------------------------------- | ---------------------------------------------------------------------- |
| One-liner guardrails                    | Step-by-step workflows                                                 |
| "Keep require() behind if/else for DCE" | Full DCE pattern with code examples, verification commands, edge cases |
| Points to skills via `$name`            | Expands on AGENTS.md rules                                             |

When adding a skill, also add a one-liner summary to the relevant AGENTS.md section with a `$skill-name` reference.

### Naming

- Short, descriptive, topic-scoped: `flags`, `dce-edge`, `react-vendoring`
- No repo prefix (already scoped by `.agents/skills/`)
- Hyphens for multi-word names

### Supplementary Files

For complex skills, use a hub + detail pattern:

```
pr-status-triage/
├── SKILL.md         # Overview, quick commands, links to details
├── workflow.md      # Prioritization and patterns
└── local-repro.md   # CI env matching
```

<!-- chapter:end slug=authoring-skills -->

---

<!-- chapter:begin slug=backport-pr position=2 -->

## 2. backport-pr

- **Source:** https://github.com/vercel/next.js/blob/canary/.agents/skills/backport-pr/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/backport-pr/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/backport-pr.md
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: backport-pr
description: >
  Backport a merged Next.js pull request from canary to a previous release
  branch such as next-16-2. Use when the user asks to backport, cherry-pick,
  or open a backport PR from a PR number to an older Next.js version. Covers
  finding the merged PR commit, creating a backport branch from the target
  release branch, cherry-picking from canary, validating, and opening the PR
  with the release branch as the base.
metadata:
  internal: true
---

# Backport PR

Use this skill when a user asks to backport a merged Next.js PR to a release
branch.

## Inputs

- Require a PR number and a target release branch, for example `next-16-2`.
- If the target branch is not provided and cannot be inferred confidently from
  the user's request, ask before mutating git state.
- Treat the target branch as variable; do not hard-code `next-16-2` except when
  the user explicitly asks for it.

## Workflow

1. Inspect the current worktree before changing branches:

   ```bash
   git status --short
   git branch --show-current
   ```

   Preserve unrelated user changes. Do not overwrite, reset, or stash them
   without the user's consent.

2. Sync the source and target branches:

   ```bash
   git fetch origin canary:refs/remotes/origin/canary <target-branch>:refs/remotes/origin/<target-branch>
   ```

3. Identify the commit that landed the PR on `canary`:

   ```bash
   gh pr view <pr-number> --repo vercel/next.js --json number,title,state,url,mergeCommit,baseRefName,headRefName
   git log origin/canary --oneline --fixed-strings --grep="(#<pr-number>)"
   ```

   Prefer `mergeCommit.oid` when the PR is `MERGED` and the commit is contained
   in `origin/canary`. If GitHub does not return a usable merge commit, use the
   `git log --grep` result and verify the commit subject references the PR
   number.

4. Create the backport branch from the release branch:

   ```bash
   git switch -c backport-<pr-number>-to-<target-branch> origin/<target-branch>
   ```

   After switching branches in this repo, run `pnpm build-all` before Next.js
   integration tests unless the user explicitly limits the task to preparing the
   cherry-pick or PR.

5. Cherry-pick the landed commit with provenance:

   ```bash
   git cherry-pick -x <merged-commit-sha>
   ```

   Resolve conflicts in favor of preserving the release branch's compatibility
   constraints. If the cherry-pick is empty, verify whether the change is already
   present on the release branch and report that instead of opening a duplicate
   PR.

6. Verify with the narrowest commands that cover the touched files. Prefer
   focused tests, `pnpm types` for TypeScript-only risk, and the relevant
   integration test mode for behavior changes.

7. Open the backport PR using `$create-pr`.

   Override the normal `$create-pr` base branch: use `--base <target-branch>`,
   not `canary`. Keep the PR as a draft unless the user explicitly asks
   otherwise.

## PR Shape

Use a title like:

```text
[backport] <original PR title>
```

Use a concise PR body:

```markdown
Backports <original PR title/link> to `<target-branch>`.

<!-- NEXT_JS_LLM -->
```

## Related Skills

- `$create-pr` - Create the branch commit, push it, and open the draft PR.
- `$pr-status-triage` - Check CI failures or review feedback after the PR exists.

<!-- chapter:end slug=backport-pr -->

---

<!-- chapter:begin slug=create-pr position=3 -->

## 3. create-pr

- **Source:** https://github.com/vercel/next.js/blob/canary/.agents/skills/create-pr/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/create-pr/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/create-pr.md
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: create-pr
description: >
  Create Git branches, commits, pushes, and GitHub pull requests for Next.js.
  Use when the user asks to create a branch, commit current changes, open a
  PR or draft PR, publish a pull request, or recover from gh pr create / PR
  template issues. Covers .github/pull_request_template.md, --body formatting,
  codex/ branch names, and Codex app git directives.
metadata:
  internal: true
---

# Create PR

Use this skill when turning local work into a GitHub pull request.

## Fork PRs vs Branch PRs

Before writing a PR description, check whether this is a branch PR (the
branch lives in `vercel/next.js`) or a fork PR (an external contribution from
a fork). You may write full descriptions for branch PRs, but not for fork
PRs — inform the user, offer to review their description or provide technical
details, and give them the GitHub URL to create the PR themselves. See
"GitHub Pull Requests and Issues" in `AGENTS.md` for the full policy.

## Workflow

1. Inspect the current state before mutating Git:

   ```bash
   git status --short
   git branch --show-current
   git diff -- <paths>
   ```

   Stage only files that belong to the requested change. Preserve unrelated
   user changes.

2. Create or confirm the branch:

   ```bash
   git switch -c codex/<short-topic>
   ```

   Use the `codex/` prefix unless the user asks for a different name. If a
   `.git/*lock` or `Operation not permitted` error appears, rerun the same Git
   command with sandbox escalation. Do not assume a branch namespace conflict
   until checking refs with `git branch --list`, `git show-ref`, or
   `git for-each-ref`.

3. Validate and commit:

   ```bash
   git add <paths>
   git diff --cached --check
   git commit -m "<concise message>"
   ```

   Keep commit messages concise and do not add generated-tool or co-author
   footers.

4. Push the branch:

   ```bash
   git push -u origin <branch>
   ```

5. Create the PR as a draft unless the user explicitly asks otherwise:

   ```bash
   gh pr create --draft --base canary --head <branch> --title "<title>" --body '<body>'
   ```

   For this repo, prefer `canary` as the base branch. If GitHub network access
   fails in the sandbox, rerun with escalation.

## PR Body

Use this PR body format:

```markdown
## Summary

<what changed and why>

## Verification

- `<command that passed>`
- Not run: `<command>` (`<reason>`)

<!-- NEXT_JS_LLM -->
```

The "what" should be explained from the end-user perspective or developer perspective. Only include implementation changes if they're not obvious from the diff.

A "why" should be included if the change isn't self-explanatory, or if the motivation is not clear from the diff.
Omitting the "why" should be used sparingly and only for small changes.

Do not include trivial verification commands that CI already covers (e.g. `pnpm run build`), but do include any manual verification steps. If the PR changes a test, you don't need to repeat the command to run that test. But if you used an existing test to validate some behavior didn't change, include that test.

Use `--body` with this filled content.

## Recovery

- If a PR may already exist, check before creating a duplicate:

  ```bash
  gh pr view --head <branch> --json url,isDraft,title
  ```

- If approval is denied for `gh pr create`, report that the branch is pushed
  but the PR was not created, and provide the exact corrected command.
- After successful Codex app Git actions, include the appropriate final-response
  directives for branch creation, staging, committing, pushing, and PR creation.

## Related Skills

- `$pr-status-triage` - Analyze CI failures and PR review feedback after the PR exists.
- `$gh-stack` - Manage stacked branches and dependent pull requests.

<!-- chapter:end slug=create-pr -->

---

<!-- chapter:begin slug=dce-edge position=4 -->

## 4. dce-edge

- **Source:** https://github.com/vercel/next.js/blob/canary/.agents/skills/dce-edge/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/dce-edge/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/dce-edge.md
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: dce-edge
description: >
  DCE-safe require() patterns and edge runtime constraints. Use when writing
  conditional require() calls, guarding Node-only imports (node:stream etc.),
  or editing define-env-plugin.ts / app-render / stream-utils for edge builds.
  Covers if/else branching for webpack DCE, TypeScript definite assignment,
  the NEXT_RUNTIME vs real feature flag distinction, and forcing flags false
  for edge in define-env.ts.
metadata:
  internal: true
---

# DCE + Edge

Use this skill when changing conditional `require()` paths, Node-only imports, or edge/runtime branching.

## DCE-Safe `require()` Pattern

Webpack only DCEs a `require()` when it sits inside the dead branch of an `if/else` whose condition DefinePlugin can evaluate at compile time.

```ts
// CORRECT - webpack can eliminate the dead branch
if (process.env.__NEXT_USE_NODE_STREAMS) {
  require('node:stream')
} else {
  // web path
}
```

What does NOT work:

- **Early-return/throw guards**: webpack doesn't do control-flow analysis for throws/returns, so the `require()` is still traced.
- **Bare `if` without `else`**: works for inline `node:*` specifiers but NOT for `require('./some-module')` that pulls a new file into the module graph.

Always test edge changes with `pnpm test-start-webpack` on `test/e2e/app-dir/app/standalone.test.ts` (has edge routes), not with `NEXT_SKIP_ISOLATE=1` which skips the full webpack compilation.

## TypeScript + DCE Interaction

Use `if/else` (not two independent `if` blocks) when assigning a variable conditionally on `process.env.X`. TypeScript cannot prove exhaustiveness across `if (flag) { x = a }; if (!flag) { x = b }` and will error with "variable used before being assigned". The `if/else` pattern satisfies both TypeScript (definite assignment) and webpack DCE.

## Compile-Time Switcher Pattern

Platform-specific code (node vs web) can use a single `.ts` switcher module that conditionally `require()`s either `.node.ts` or `.web.ts` into a typed variable, then re-exports the shared runtime API as named exports. Keep the branch as `if/else` so DefinePlugin can dead-code-eliminate the unused `require()`. Keep shared types canonical in `.node.ts`, with `.web.ts` importing them via `import type` and the switcher re-exporting types as needed. Examples: `stream-ops.ts` and `debug-channel-server.ts`.

## `NEXT_RUNTIME` Is Not a Feature Flag

In user-project webpack server compilers, `process.env.NEXT_RUNTIME` is inlined to `'nodejs'`. Guarding Node-only `require('node:*')` paths with `NEXT_RUNTIME === 'nodejs'` does **not** prune anything. For feature-gated codepaths, guard on the real feature define (e.g. `process.env.__NEXT_USE_NODE_STREAMS`).

## Edge Runtime Constraints

Edge routes do NOT use pre-compiled runtime bundles. They are compiled by the user's webpack/Turbopack, so `define-env.ts` controls DCE. Feature flags that gate `node:*` imports must be forced to `false` for edge builds in `define-env.ts` (`isEdgeServer ? false : flagValue`), otherwise webpack will try to resolve `node:stream` etc. and fail.

## `app-page.ts` Template Gotchas

- `app-page.ts` is a build template compiled by the user's bundler. Any `require()` in this file is traced by webpack/turbopack at `next build` time. You cannot require internal modules with relative paths because they won't be resolvable from the user's project. Instead, export new helpers from `entry-base.ts` and access them via `entryBase.*` in the template.
- Template helpers should stay out of `RenderResult`. If `app-page.ts` needs a Node-stream-only utility, prefer a small dedicated helper module in `server/stream-utils/` (with DCE-safe `if/else` + `require()`).

## Verification

- Validate edge bundling regressions with `pnpm test-start-webpack test/e2e/app-dir/app/standalone.test.ts`
- For module-resolution/build-graph fixes, verify without `NEXT_SKIP_ISOLATE=1`

## Related Skills

- `$flags` - flag wiring (config/schema/define-env/runtime env)
- `$react-vendoring` - entry-base boundaries and vendored React
- `$runtime-debug` - reproduction and verification workflow

<!-- chapter:end slug=dce-edge -->

---

<!-- chapter:begin slug=flags position=5 -->

## 5. flags

- **Source:** https://github.com/vercel/next.js/blob/canary/.agents/skills/flags/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/flags/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/flags.md
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: flags
description: >
  How to add or modify Next.js experimental feature flags end-to-end.
  Use when editing config-shared.ts, config-schema.ts, define-env-plugin.ts,
  next-server.ts, export/worker.ts, or module.compiled.js. Covers type
  declaration, zod schema, build-time injection, runtime env plumbing,
  and the decision between runtime env-var branching vs separate bundle variants.
metadata:
  internal: true
---

# Feature Flags

Use this skill when adding or changing framework feature flags in Next.js internals.

## Required Wiring

All flags need: `config-shared.ts` (type) → `config-schema.ts` (zod). If the flag is consumed in user-bundled code (client components, edge routes, `app-page.ts` template), also add it to `define-env.ts` for build-time injection. Runtime-only flags consumed exclusively in pre-compiled bundles can skip `define-env.ts`.

## Where the Flag Is Consumed

**Client/bundled code only** (e.g. `__NEXT_PPR` in client components): `define-env.ts` is sufficient. Webpack/Turbopack replaces `process.env.X` at the user's build time.

**Pre-compiled runtime bundles** (e.g. code in `app-render.tsx`): The flag must also be set as a real `process.env` var at runtime, because `app-render.tsx` runs from pre-compiled bundles where `define-env.ts` doesn't reach. Two approaches:

- **Runtime env var**: Set in `next-server.ts` + `export/worker.ts`. Both code paths stay in one bundle. Simple but increases bundle size.
- **Separate bundle variant**: Add DefinePlugin entry in `next-runtime.webpack-config.js` (scoped to `bundleType === 'app'`), new taskfile tasks, update `module.compiled.js` selector, and still set env var in `next-server.ts` + `export/worker.ts` for bundle selection. Eliminates dead code but adds build complexity.

For runtime flags, also add the field to the `NextConfigRuntime` Pick type in `config-shared.ts`.

## Runtime-Bundle Model

- Runtime bundles are built by `next-runtime.webpack-config.js` (rspack) via `taskfile.js` bundle tasks.
- Bundle selection occurs at runtime in `src/server/route-modules/app-page/module.compiled.js` based on `process.env` vars.
- Variants: `{turbo/webpack} × {experimental/stable/nodestreams/experimental-nodestreams} × {dev/prod}` = up to 16 bundles per route type.
- `define-env.ts` affects user bundling, not pre-compiled runtime internals.
- `process.env.X` checks in `app-render.tsx` are either replaced by DefinePlugin at runtime-bundle-build time, or read as actual env vars at server startup. They are NOT affected by the user's defines from `define-env.ts`.
- **Gotcha**: DefinePlugin entries in `next-runtime.webpack-config.js` must be scoped to the correct `bundleType` (e.g. `app` only, not `server`) to avoid replacing assignment targets in `next-server.ts`.

## Related Skills

- `$dce-edge` - DCE-safe require patterns and edge constraints
- `$react-vendoring` - entry-base boundaries and vendored React
- `$runtime-debug` - reproduction and verification workflow

<!-- chapter:end slug=flags -->

---

<!-- chapter:begin slug=gh-stack position=6 -->

## 6. gh-stack

- **Source:** https://github.com/vercel/next.js/blob/canary/.agents/skills/gh-stack/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/gh-stack/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/gh-stack.md
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

Bundled files (3), referenced from this skill's directory:
  - `references/commands.md` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/gh-stack/references/commands.md
  - `references/stack-design.md` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/gh-stack/references/stack-design.md
  - `references/troubleshooting.md` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/gh-stack/references/troubleshooting.md

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: gh-stack
description: >
  Manages stacked PRs and splits multi-part work into reviewable branches with gh-stack.
  Use for stack creation, viewing, edits, push, submit, sync, rebase, merge, or checkout;
  when asked to split or isolate work for review; whenever a user mentions a stack,
  branch layers, dependent PRs, or gh stack; or when a stack is checked out.
metadata:
  author: github
  version: '0.1.0'
---

# gh-stack

`gh stack` is a [GitHub CLI](https://cli.github.com/) extension for stacked branches and pull
requests. A stack is an ordered chain of branches rooted on a trunk, where each branch has one PR
based on the branch below it, so a reviewer sees only that layer's diff.

`gh stack` prints a stack trunk-first, left to right:

```
(main) <- auth <- api <- frontend
```

Left is the **bottom**, right is the **top**. `auth` is based on `main` and merges first;
`frontend` merges last. `up` moves toward the top, away from trunk; `down` moves toward it.
Foundational work belongs at the bottom, code that depends on it above. For how to choose the
layers, read `references/stack-design.md`.

## Setup

```bash
gh extension install github/gh-stack
git config rerere.enabled true         # remember conflict resolutions
git config remote.pushDefault origin   # required if the repo has more than one remote
```

## Non-interactive use

`gh stack` branches on whether **stdout is a TTY**. Piped, most commands error cleanly or print
static text; under a PTY the same commands open a prompt or a full-screen TUI and block forever.
Agent harnesses differ, so always pass the flags below instead of relying on that detection.

**Multiple remotes:** never run `push`, `submit`, `sync`, `rebase`, or `link` without
`--remote <name>` unless `remote.pushDefault` is configured. `checkout` and `trunk` have no
`--remote` flag and require the config.

| Always run                                | Never run bare      | Why                                           |
| ----------------------------------------- | ------------------- | --------------------------------------------- |
| `gh stack view --json`                    | `gh stack view`     | opens a TUI under a PTY                       |
| `gh stack submit --auto`                  | `gh stack submit`   | prompts for a title per new PR                |
| `gh stack merge <target> --yes`           | `gh pr merge`       | `gh pr merge` cannot merge a stack            |
| `gh stack init <branch>...`               | `gh stack init`     | prompts for branch names                      |
| `gh stack add <branch>`                   | `gh stack add`      | prompts for a name, and fails even when piped |
| `gh stack checkout <target>`              | `gh stack checkout` | opens a selection menu                        |
| `gh stack up` / `down` / `top` / `bottom` | `gh stack switch`   | `switch` is menu-only                         |
| —                                         | `gh stack modify`   | TUI-only, no non-interactive path             |

- `view --short` is safe in both modes, but it is formatted for humans. Use `--json` to parse.
- **`checkout <pr>` when a different local stack already covers those branches** cannot be forced.
  Run `gh stack unstack --local` first (this keeps the stack on GitHub), then retry.

## Branch placement

- **Starting multi-part work:** create the stack before writing files. Do not implement every
  concern on trunk and split it later. Put one dependent concern in each layer, bottom to top.
- **Editing an existing stack:** check out the layer that owns the change before editing. Never
  commit a lower layer's concern on the current top branch. Run `gh stack view --json`; if
  ownership is unclear, inspect `git log --all -- <path>`. Then check out the owner, edit, commit,
  rebase upstack, and return to top.

```bash
gh stack down                   # or: gh stack checkout api
git add ... && git commit -m "Add get-user endpoint"
gh stack rebase --upstack       # replay every branch above onto the change
gh stack top                    # return to where you were
gh stack push
```

## Core loop

```bash
gh stack init auth              # create the stack and check out its branch
git add ... && git commit -m "Add auth middleware"
gh stack add api                # next layer, branched from the current one
git add ... && git commit -m "Add API routes"
gh stack submit --auto          # push every branch and open draft PRs
gh stack view --json            # confirm
```

Add `--open` to `submit` to create PRs ready for review instead of drafts. Branch names are
verbatim — `gh stack add refactor/foo` creates `refactor/foo`.

## Staying in sync

```bash
gh stack sync                   # fetch, reconcile with GitHub, rebase, push, refresh PR state
gh stack sync --prune           # also delete local branches for merged PRs
```

Pruning never happens without `--prune` when non-interactive. If the local and remote stacks have
diverged, `sync` prints both chains, makes no changes, and exits 0 with `Sync aborted` — see
`references/troubleshooting.md`.

## Merging

Scope the merge with an argument:

```bash
gh stack merge 42 --yes          # PR #42 plus every unmerged PR below it
gh stack merge 7 --yes           # every unmerged PR in stack #7
gh stack merge 42 --yes --squash # or --merge, --rebase, --merge-method <method>
```

Pass a PR number to merge that PR and every unmerged PR below it, or a stack number to merge every
unmerged PR in that stack. The operation is all-or-nothing: if any PR in that set cannot merge,
none do.

Without a method flag the last-used method is reused. If the base branch uses a merge queue, the
stack is queued instead and the queue picks the method, ignoring any flag you passed with a
warning; queued PRs may land in separate groups.

## Reading state

`gh stack view --json` writes JSON to **stdout**. Status messages go to **stderr** — do not parse
them, branch on exit codes instead.

```
trunk           string
currentBranch   string
branches[]      name, head, base, isCurrent, isMerged, isQueued, needsRebase
branches[].pr   number, url, state ("OPEN" | "MERGED" | "QUEUED"); absent when no PR exists
```

`base` is the saved SHA of the parent branch that this branch was last known to contain. It may be
older than the parent's current tip. `needsRebase` is true when the current parent tip is no longer
an ancestor of the branch.

## Exit codes

| Code | Meaning                    | Recovery                                                   |
| ---- | -------------------------- | ---------------------------------------------------------- |
| 0    | Success                    | —                                                          |
| 1    | Generic error              | Read stderr                                                |
| 2    | Not in a stack             | `gh stack init`, or `gh stack checkout <target>`           |
| 3    | Rebase conflict            | Follow the Exit 3 recovery below                           |
| 4    | GitHub API failure         | Check `gh auth status`, retry                              |
| 5    | Invalid arguments          | Fix the invocation; see `<command> --help`                 |
| 6    | Disambiguation required    | Branch is in several stacks; check out a non-shared branch |
| 7    | Rebase already in progress | `gh stack rebase --continue` or `--abort`                  |
| 8    | Stack file locked          | Another `gh stack` process is writing; retry after ~5s     |
| 9    | Stacked PRs unavailable    | Not enabled on the repository; tell the user               |
| 10   | Modify recovery required   | `gh stack modify --abort`                                  |

**Exit 3 recovery:**

- After `gh stack rebase`: resolve the files, run `git add`, then
  `gh stack rebase --continue`; use `gh stack rebase --abort` to restore the stack.
- After `gh stack sync`: the stack has already been restored. Run `gh stack rebase` to recreate the
  conflict, then resolve and continue as above.

## Constraints

- Stacks are strictly linear: one parent, at most one child. Use separate stacks for parallel work.
- There is no non-interactive reorder or removal. Errors may suggest `gh stack modify`, but it is
  TUI-only — restructure with `unstack` then `init` instead.
- PR titles and bodies are auto-generated. Use `gh pr edit` afterwards to change them.
- `checkout <branch-name>` resolves against local stacks only. Use a stack or PR number to pull a
  stack down from GitHub.

## More detail

`gh stack <command> --help` is authoritative for flags and arguments. Note that
`gh stack help <command>` does **not** work — it prints the top-level help.

Open the reference whose trigger matches the task; no need to preload all three.

- `references/stack-design.md` — read before creating a stack, when deciding how many layers to
  use, what belongs in each one, or whether work belongs in a new stack.
- `references/commands.md` — read when a command fails unexpectedly or you need its preconditions,
  side effects, atomicity, or ordering guarantees.
- `references/troubleshooting.md` — read on a rebase conflict, after a squash-merge, on local and
  remote divergence, when restructuring a stack, or when driving stacks from another tool.

<!-- chapter:end slug=gh-stack -->

---

<!-- chapter:begin slug=pr-status-triage position=7 -->

## 7. pr-status-triage

- **Source:** https://github.com/vercel/next.js/blob/canary/.agents/skills/pr-status-triage/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/pr-status-triage/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/pr-status-triage.md
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

Bundled files (2), referenced from this skill's directory:
  - `local-repro.md` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/pr-status-triage/local-repro.md
  - `workflow.md` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/pr-status-triage/workflow.md

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: pr-status-triage
description: >
  Triage CI failures and PR review comments using scripts/pr-status.js.
  Use when investigating failing CI jobs, flaky tests, or PR review feedback.
  Covers blocker-first prioritization (build > lint > types > tests),
  CI env var matching for local reproduction, and the Known Flaky Tests
  distinction.
metadata:
  internal: true
---

# PR Status Triage

Use this skill when the user asks about PR status, CI failures, or review comments in the Next.js monorepo.

## Workflow

1. Run `node scripts/pr-status.js --wait` in the background (timeout 1 min), then read `scripts/pr-status/results/index.md`.
2. Analyze each `job-{id}.md` and `thread-{N}.md` file in `scripts/pr-status/results/` for failures and review feedback.
3. Prioritize blocking jobs first: build, lint, types, then test jobs.
4. Treat failures as real until disproven; check the "Known Flaky Tests" section before calling anything flaky.
5. Reproduce locally with the same mode and env vars as CI.
6. After addressing review comments, reply to the thread describing what was done, then resolve it. Use `reply-and-resolve-thread` to do both in one step, or use `reply-thread` + `resolve-thread` separately. See `scripts/pr-status/results/thread-N.md` files for ready-to-use commands.
7. When the only remaining failures are known flaky tests and no code changes are needed, retrigger the failing CI jobs with `gh run rerun <run-id> --failed`. Then wait 5 minutes and go back to step 1. Repeat this loop up to 5 times.

## Quick Commands

```bash
node scripts/pr-status.js                  # current branch PR
node scripts/pr-status.js <number>         # specific PR
node scripts/pr-status.js [PR] --wait      # background mode, waits for CI to finish
node scripts/pr-status.js --skip-flaky-check  # skip flaky test detection
```

Thread interaction:

```bash
node scripts/pr-status.js reply-thread <threadNodeId> "<body>"           # reply to a review thread
node scripts/pr-status.js resolve-thread <threadNodeId>                  # resolve a review thread
node scripts/pr-status.js reply-and-resolve-thread <threadNodeId> "<body>"  # reply and resolve in one step
```

## References

- [workflow.md](./workflow.md) — prioritization, common failure patterns, resolving review threads
- [local-repro.md](./local-repro.md) — mode/env matching and isolation guidance

<!-- chapter:end slug=pr-status-triage -->

---

<!-- chapter:begin slug=react-vendoring position=8 -->

## 8. react-vendoring

- **Source:** https://github.com/vercel/next.js/blob/canary/.agents/skills/react-vendoring/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/react-vendoring/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/react-vendoring.md
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: react-vendoring
description: >
  React vendoring and react-server layer boundaries. Use when editing
  entry-base.ts, $$compiled.internal.d.ts, compiled/react* packages,
  or taskfile.js copy_vendor_react. Covers the entry-base.ts boundary
  (all react-server-dom-webpack/* imports must go through it), vendored
  React channels, type declarations, Turbopack remap to
  react-server-dom-turbopack, ComponentMod access patterns, and ESLint
  suppression for guarded requires.
metadata:
  internal: true
---

# React Vendoring

Use this skill for changes touching vendored React, `react-server-dom-webpack/*`, or react-server layer boundaries.

## App Router Vendoring

React is NOT resolved from `node_modules` for App Router. It's vendored into `packages/next/src/compiled/` during `pnpm build` (task: `copy_vendor_react()` in `taskfile.js`). Pages Router resolves React from `node_modules` normally.

- **Two channels**: stable (`compiled/react/`) and experimental (`compiled/react-experimental/`). The runtime bundle webpack config aliases to the correct channel via `makeAppAliases({ experimental })`.

## `entry-base.ts` Boundary

Only `entry-base.ts` is compiled in rspack's `(react-server)` layer. ALL imports from `react-server-dom-webpack/*` (Flight server/static APIs) must go through `entry-base.ts`. Other files like `stream-ops.node.ts` or `app-render.tsx` must access Flight APIs via the `ComponentMod` parameter (which is the `entry-base.ts` module exposed through the `app-page.ts` build template).

Direct imports from `react-server-dom-webpack/server.node` or `react-server-dom-webpack/static` in files outside `entry-base.ts` will fail at runtime with "The react-server condition must be enabled". Dev mode may mask this error, but production workers fail immediately.

## Type Declarations

`packages/next/types/$$compiled.internal.d.ts` contains `declare module` blocks for vendored React packages. When adding new APIs (e.g. `renderToPipeableStream`, `prerenderToNodeStream`), you must add type declarations here. The bare specifier types (e.g. `declare module 'react-server-dom-webpack/server'`) are what source code in `src/` imports against.

## Adding Node.js-Only React APIs

These exist in `.node` builds but not in the type definitions. Steps:

1. Add type declarations to `$$compiled.internal.d.ts`.
2. Export the API from `entry-base.ts` behind a `process.env` guard.
3. Access it via `ComponentMod` in other files.

```typescript
// In entry-base.ts (react-server layer) only:
/* eslint-disable import/no-extraneous-dependencies */
export let renderToPipeableStream: ... | undefined
if (process.env.__NEXT_USE_NODE_STREAMS) {
  renderToPipeableStream = (
    require('react-server-dom-webpack/server.node') as typeof import('react-server-dom-webpack/server.node')
  ).renderToPipeableStream
} else {
  renderToPipeableStream = undefined
}
/* eslint-enable import/no-extraneous-dependencies */

// In other files, access via ComponentMod:
ComponentMod.renderToPipeableStream!(payload, clientModules, opts)
```

## ESLint Practical Rule

For guarded runtime `require()` blocks that need `import/no-extraneous-dependencies` suppression, prefer scoped block disable/enable. If using `eslint-disable-next-line`, the comment must be on the line immediately before the `require()` call, NOT before the `const` declaration. When the `const` and `require()` are on different lines, this is error-prone.

## Turbopack Remap

`react-server-dom-webpack/*` is silently remapped to `react-server-dom-turbopack/*` by Turbopack's import map. Code says "webpack" everywhere, but Turbopack gets its own bindings at runtime. This affects debugging: stack traces and error messages will reference the turbopack variant.

## Related Skills

- `$flags` - flag wiring (config/schema/define-env/runtime env)
- `$dce-edge` - DCE-safe require patterns and edge constraints
- `$runtime-debug` - reproduction and verification workflow

<!-- chapter:end slug=react-vendoring -->

---

<!-- chapter:begin slug=router-act position=9 -->

## 9. router-act

- **Source:** https://github.com/vercel/next.js/blob/canary/.agents/skills/router-act/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/router-act/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/router-act.md
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: router-act
description: >
  How to write end-to-end tests using createRouterAct and LinkAccordion.
  Use when writing or modifying tests that need to control the timing of
  internal Next.js requests (like prefetches) or assert on their responses.
  Covers the act API, fixture patterns, prefetch control via LinkAccordion,
  fake clocks, and avoiding flaky testing patterns.
user-invocable: false
metadata:
  internal: true
---

# Router Act Testing

Use this skill when writing or modifying tests that involve prefetch requests, client router navigations, or the segment cache. The `createRouterAct` utility from `test/lib/router-act.ts` lets you assert on prefetch and navigation responses in an end-to-end way without coupling to the exact number of requests or the protocol details. This is why most client router-related tests use this pattern.

## When NOT to Use `act`

Don't bother with `act` if you don't need to instrument the network responses — either to control their timing or to assert on what's included in them. If all you're doing is waiting for some part of the UI to appear after a navigation, regular Playwright helpers like `browser.elementById()`, `browser.elementByCss()`, and `browser.waitForElementByCss()` are sufficient.

## Core Principles

1. **Use `LinkAccordion` to control when prefetches happen.** Never let links be visible outside an `act` scope.
2. **Prefer `'no-requests'`** whenever the data should be served from cache. This is the strongest assertion — it proves the cache is working.
3. **Avoid retry/polling timers.** The `act` utility exists specifically to replace inherently flaky patterns like `retry()` loops or `setTimeout` waits for network activity. If you find yourself wanting to poll, you're probably not using `act` correctly.
4. **Avoid the `block` feature.** It's prone to false negatives. Prefer `includes` and `'no-requests'` assertions instead.

## Act API

### Config Options

```typescript
// Assert NO router requests are made (data served from cache).
// Prefer this whenever possible — it's the strongest assertion.
await act(async () => { ... }, 'no-requests')

// Expect at least one response containing this substring
await act(async () => { ... }, { includes: 'Page content' })

// Expect multiple responses (checked in order)
await act(async () => { ... }, [
  { includes: 'First response' },
  { includes: 'Second response' },
])

// Assert the same content appears in two separate responses
await act(async () => { ... }, [
  { includes: 'Repeated content' },
  { includes: 'Repeated content' },
])

// Expect at least one request, don't assert on content
await act(async () => { ... })
```

### How `includes` Matching Works

- The `includes` substring is matched against the HTTP response body. Use text content that appears literally in the rendered output (e.g. `'Dynamic content (stale time 60s)'`).
- Extra responses that don't match any `includes` assertion are silently ignored — you only need to assert on the responses you care about. This keeps tests decoupled from the exact number of requests the router makes.
- Each `includes` expectation claims exactly one response. If the same substring appears in N separate responses, provide N separate `{ includes: '...' }` entries.

### App Shell requests are ignored by default

When App Shells are enabled (the default when Cache Components is on), a `prefetch` is split into two phases: an **App Shell** prefetch — the param/searchParam-independent chrome of the route (layouts, loading boundaries, static shell) — and a separate per-link/per-page data prefetch. The App Shell is conceptually part of the route, not prefetch data, so **`act` ignores App Shell requests for all assertion purposes** (they carry a `next-router-prefetch: '3'` header).

This means you generally do **not** need to account for the extra App Shell response in your assertions. If a `Loading...` fallback now arrives in both the App Shell prefetch and the per-link prefetch, you still write a single `{ includes: 'Loading...' }` — the App Shell copy is invisible to matching. Likewise, `'no-requests'` still passes even if an App Shell prefetch fires, and `block: 'reject'` won't match content that appears only in the App Shell.

App Shell requests are still intercepted, fulfilled, and awaited (so the shell is cached and no requests are left in flight) — they just don't participate in `includes` matching, `no-requests`, `block: 'reject'`, or the "at least one request" check. An App Shell response that returns an error status (4xx/5xx) still fails the test.

To assert on App Shell responses directly — for tests specifically about App Shell behavior — opt in at the `act` instance level:

```typescript
const act = createRouterAct(page, { includeAppShellRequests: true })
```

With this option, App Shell requests are treated like any other router request. Prefer expressing App Shell behavior through observable outcomes (e.g. an instant navigation rendering the cached shell before the data response arrives) rather than asserting on prefetch content where practical. See `test/e2e/app-dir/segment-cache/prefetch-app-shell/prefetch-app-shell.test.ts` for the canonical example.

### What `act` Does Internally

`act` intercepts all router requests — prefetches, navigations, and Server Actions — made during the scope:

1. Installs a Playwright route handler to intercept router requests
2. Runs your scope function
3. Waits for a `requestIdleCallback` (captures IntersectionObserver-triggered prefetches)
4. Fulfills buffered responses to the browser
5. Repeats steps 3-4 until no more requests arrive
6. Asserts on the responses based on the config

Responses are buffered and only forwarded to the browser after the scope function returns. This means you cannot navigate to a new page and wait for it to render within the same scope — that would deadlock. Trigger the navigation (click the link) and let `act` handle the rest. Read destination page content _after_ `act` returns:

```typescript
await act(
  async () => {
    /* toggle accordion, click link */
  },
  { includes: 'Page content' }
)

// Read content after act returns, not inside the scope
expect(await browser.elementById('my-content').text()).toBe('Page content')
```

## LinkAccordion Pattern

### Why LinkAccordion Exists

`LinkAccordion` controls when `<Link>` components enter the DOM. A Next.js `<Link>` triggers a prefetch when it enters the viewport (via IntersectionObserver). By hiding the Link behind a checkbox toggle, you control exactly when prefetches happen — only when you explicitly toggle the accordion inside an `act` scope.

```tsx
// components/link-accordion.tsx
'use client'
import Link from 'next/link'
import { useState } from 'react'

export function LinkAccordion({ href, children, prefetch }) {
  const [isVisible, setIsVisible] = useState(false)
  return (
    <>
      <input
        type="checkbox"
        checked={isVisible}
        onChange={() => setIsVisible(!isVisible)}
        data-link-accordion={href}
      />
      {isVisible ? (
        <Link href={href} prefetch={prefetch}>
          {children}
        </Link>
      ) : (
        `${children} (link is hidden)`
      )}
    </>
  )
}
```

### Standard Navigation Pattern

Always toggle the accordion and click the link inside the same `act` scope:

```typescript
await act(
  async () => {
    // 1. Toggle accordion — Link enters DOM, triggers prefetch
    const toggle = await browser.elementByCss(
      'input[data-link-accordion="/target-page"]'
    )
    await toggle.click()

    // 2. Click the now-visible link — triggers navigation
    const link = await browser.elementByCss('a[href="/target-page"]')
    await link.click()
  },
  { includes: 'Expected page content' }
)
```

## Common Sources of Flakiness

### Using `browser.back()` with open accordions

Do not use `browser.back()` to return to a page where accordions were previously opened. BFCache restores the full React state including `useState` values, so previously-opened Links are immediately visible. This triggers IntersectionObserver callbacks outside any `act` scope — if the cached data is stale, uncontrolled re-prefetches fire and break subsequent `no-requests` assertions.

The only safe use of `browser.back()`/`browser.forward()` is when testing BFCache behavior specifically.

**Fix:** navigate forward to a fresh hub page instead. See [Hub Pages](#hub-pages).

### Using visible `<Link>` components outside `act` scopes

Any `<Link>` visible in the viewport can trigger a prefetch at any time via IntersectionObserver. If this happens outside an `act` scope, the request is uncontrolled and can interfere with subsequent assertions. Always hide links behind `LinkAccordion` and only toggle them inside `act`.

### Using retry/polling timers to wait for network activity

`retry()`, `setTimeout`, or any polling pattern to wait for prefetches or navigations to settle is inherently flaky. `act` deterministically waits for all router requests to complete before returning.

### Navigating and waiting for render in the same `act` scope

Responses are buffered until the scope exits. Clicking a link then reading destination content in the same scope deadlocks. Read page content after `act` returns instead.

## Hub Pages

When you need to navigate away from a page and come back to test staleness, use "hub" pages instead of `browser.back()`. Each hub is a fresh page with its own `LinkAccordion` components that start closed.

Hub pages use `connection()` to ensure they are dynamically rendered. This guarantees that navigating to a hub always produces a router request, which lets `act` properly manage the navigation and wait for the page to fully render before continuing.

**Hub page pattern:**

```tsx
// app/my-test/hub-a/page.tsx
import { Suspense } from 'react'
import { connection } from 'next/server'
import { LinkAccordion } from '../../components/link-accordion'

async function Content() {
  await connection()
  return <div id="hub-a-content">Hub a</div>
}

export default function Page() {
  return (
    <>
      <Suspense fallback="Loading...">
        <Content />
      </Suspense>
      <ul>
        <li>
          <LinkAccordion href="/my-test/target-page">Target page</LinkAccordion>
        </li>
      </ul>
    </>
  )
}
```

**Target pages link to hubs via LinkAccordion too:**

```tsx
// On target pages, add LinkAccordion links to hub pages
<LinkAccordion href="/my-test/hub-a">Hub A</LinkAccordion>
```

**Test flow:**

```typescript
// 1. Navigate to target (first visit)
await act(
  async () => {
    /* toggle accordion, click link */
  },
  { includes: 'Target content' }
)

// 2. Navigate to hub-a (fresh page, all accordions closed)
await act(
  async () => {
    const toggle = await browser.elementByCss(
      'input[data-link-accordion="/my-test/hub-a"]'
    )
    await toggle.click()
    const link = await browser.elementByCss('a[href="/my-test/hub-a"]')
    await link.click()
  },
  { includes: 'Hub a' }
)

// 3. Advance time
await page.clock.setFixedTime(startDate + 60 * 1000)

// 4. Navigate back to target from hub (controlled prefetch)
await act(async () => {
  const toggle = await browser.elementByCss(
    'input[data-link-accordion="/my-test/target-page"]'
  )
  await toggle.click()
  const link = await browser.elementByCss('a[href="/my-test/target-page"]')
  await link.click()
}, 'no-requests') // or { includes: '...' } if data is stale
```

## Fake Clock Setup

Segment cache staleness tests use Playwright's clock API to control `Date.now()`:

```typescript
async function startBrowserWithFakeClock(url: string) {
  let page!: Playwright.Page
  const startDate = Date.now()

  const browser = await next.browser(url, {
    async beforePageLoad(p: Playwright.Page) {
      page = p
      await page.clock.install()
      await page.clock.setFixedTime(startDate)
    },
  })

  const act = createRouterAct(page)
  return { browser, page, act, startDate }
}
```

- `setFixedTime` changes `Date.now()` return value but timers still run in real time
- The segment cache uses `Date.now()` for staleness checks
- Advancing the clock doesn't trigger IntersectionObserver — only viewport changes do
- `setFixedTime` does NOT fire pending `setTimeout`/`setInterval` callbacks

## Reference

- `createRouterAct`: `test/lib/router-act.ts`
- `LinkAccordion`: `test/e2e/app-dir/segment-cache/staleness/components/link-accordion.tsx`
- Example tests: `test/e2e/app-dir/segment-cache/staleness/`

<!-- chapter:end slug=router-act -->

---

<!-- chapter:begin slug=runtime-debug position=10 -->

## 10. runtime-debug

- **Source:** https://github.com/vercel/next.js/blob/canary/.agents/skills/runtime-debug/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/runtime-debug/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/runtime-debug.md
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: runtime-debug
description: >
  Debug and verification workflow for runtime-bundle and module-resolution
  regressions. Use when diagnosing unexpected module inclusions, bundle
  size regressions, or CI failures related to NEXT_SKIP_ISOLATE, nft.json
  traces, or runtime bundle selection (module.compiled.js). Covers CI env
  mirroring, full stack traces via __NEXT_SHOW_IGNORE_LISTED, route trace
  inspection, and webpack stats diffing.
metadata:
  internal: true
---

# Runtime Debug

Use this skill when reproducing runtime-bundle, module-resolution, or user-bundle inclusion regressions.

## Local Repro Discipline

- Mirror CI env vars when reproducing CI failures.
- Key variables: `IS_WEBPACK_TEST=1` forces webpack (turbopack is default), `NEXT_SKIP_ISOLATE=1` skips packing next.js.
- For module-resolution validation, always rerun without `NEXT_SKIP_ISOLATE=1`.

## Stack Trace Visibility

Set `__NEXT_SHOW_IGNORE_LISTED=true` to disable the ignore-list filtering in dev server error output. By default, Next.js collapses internal frames to `at ignore-listed frames`, which hides useful context when debugging framework internals. Defined in `packages/next/src/server/patch-error-inspect.ts`.

## User-Bundle Regression Guardrail

When user `next build` starts bundling internal Node-only helpers unexpectedly:

1. Inspect route trace artifacts (`.next/server/.../page.js.nft.json`).
2. Inspect traced server chunks for forbidden internals (e.g. `next/dist/server/stream-utils/node-stream-helpers.js`, `node:stream/promises`).
3. Add a `test-start-webpack` assertion that reads the route trace and traced server chunks, and fails on forbidden internals. This validates user-project bundling (not publish-time runtime bundling).

## Bundle Tracing / Inclusion Proof

To prove what user bundling includes, emit webpack stats from the app's `next.config.js`:

```js
// next.config.js
module.exports = {
  webpack(config) {
    config.profile = true
    return config
  },
}
```

Then use `stats.toJson({ modules: true, chunks: true, reasons: true })` and diff `webpack-stats-server.json` between modes. This gives concrete inclusion reasons (e.g. which module required `node:stream/promises`) and is more reliable than analyzer HTML alone.

## Related Skills

- `$flags` - flag wiring (config/schema/define-env/runtime env)
- `$dce-edge` - DCE-safe require patterns and edge constraints
- `$react-vendoring` - entry-base boundaries and vendored React

<!-- chapter:end slug=runtime-debug -->

---

<!-- chapter:begin slug=update-docs position=11 -->

## 11. update-docs

- **Source:** https://github.com/vercel/next.js/blob/canary/.agents/skills/update-docs/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/update-docs/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/update-docs.md
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

Bundled files (2), referenced from this skill's directory:
  - `references/CODE-TO-DOCS-MAPPING.md` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/update-docs/references/CODE-TO-DOCS-MAPPING.md
  - `references/DOC-CONVENTIONS.md` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/update-docs/references/DOC-CONVENTIONS.md

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: update-docs
description: This skill should be used when the user asks to "update documentation for my changes", "check docs for this PR", "what docs need updating", "sync docs with code", "scaffold docs for this feature", "document this feature", "review docs completeness", "add docs for this change", "what documentation is affected", "docs impact", or mentions "docs/", "docs/01-app", "docs/02-pages", "MDX", "documentation update", "API reference", ".mdx files". Provides guided workflow for updating Next.js documentation based on code changes.
metadata:
  internal: true
---

# Next.js Documentation Updater

Guides you through updating Next.js documentation based on code changes on the active branch. Designed for maintainers reviewing PRs for documentation completeness.

## Quick Start

1. **Analyze changes**: Run `git diff canary...HEAD --stat` to see what files changed
2. **Identify affected docs**: Map changed source files to documentation paths
3. **Review each doc**: Walk through updates with user confirmation
4. **Validate**: Run `pnpm lint` to check formatting
5. **Commit**: Stage documentation changes

## Workflow: Analyze Code Changes

### Step 1: Get the diff

```bash
# See all changed files on this branch
git diff canary...HEAD --stat

# See changes in specific areas
git diff canary...HEAD -- packages/next/src/
```

### Step 2: Identify documentation-relevant changes

Look for changes in these areas:

| Source Path                            | Likely Doc Impact           |
| -------------------------------------- | --------------------------- |
| `packages/next/src/client/components/` | Component API reference     |
| `packages/next/src/server/`            | Function API reference      |
| `packages/next/src/shared/lib/`        | Varies by export            |
| `packages/next/src/build/`             | Configuration or build docs |
| `packages/next/src/lib/`               | Various features            |

### Step 3: Map to documentation files

Use the code-to-docs mapping in `references/CODE-TO-DOCS-MAPPING.md` to find corresponding documentation files.

Example mappings:

- `src/client/components/image.tsx` → `docs/01-app/03-api-reference/02-components/image.mdx`
- `src/server/config-shared.ts` → `docs/01-app/03-api-reference/05-config/`

## Workflow: Update Existing Documentation

### Step 1: Read the current documentation

Before making changes, read the existing doc to understand:

- Current structure and sections
- Frontmatter fields in use
- Whether it uses `<AppOnly>` / `<PagesOnly>` for router-specific content

### Step 2: Identify what needs updating

Common updates include:

- **New props/options**: Add to the props table and create a section explaining usage
- **Changed behavior**: Update descriptions and examples
- **Deprecated features**: Add deprecation notices and migration guidance
- **New examples**: Add code blocks following conventions

### Step 3: Apply updates with confirmation

For each change:

1. Show the user what you plan to change
2. Wait for confirmation before editing
3. Apply the edit
4. Move to the next change

### Step 4: Check for shared content

If the doc uses the `source` field pattern (common for Pages Router docs), the source file is the one to edit. Example:

```yaml
# docs/02-pages/... file with shared content
---
source: app/building-your-application/optimizing/images
---
```

Edit the App Router source, not the Pages Router file.

### Step 5: Validate changes

```bash
pnpm lint          # Check formatting
pnpm prettier-fix  # Auto-fix formatting issues
```

## Workflow: Scaffold New Feature Documentation

Use this when adding documentation for entirely new features.

### Step 1: Determine the doc type

| Feature Type        | Doc Location                                        | Template         |
| ------------------- | --------------------------------------------------- | ---------------- |
| New component       | `docs/01-app/03-api-reference/02-components/`       | API Reference    |
| New function        | `docs/01-app/03-api-reference/04-functions/`        | API Reference    |
| New config option   | `docs/01-app/03-api-reference/05-config/`           | Config Reference |
| New concept/guide   | `docs/01-app/02-guides/`                            | Guide            |
| New file convention | `docs/01-app/03-api-reference/03-file-conventions/` | File Convention  |

### Step 2: Create the file with proper naming

- Use kebab-case: `my-new-feature.mdx`
- Add numeric prefix if ordering matters: `05-my-new-feature.mdx`
- Place in the correct directory based on feature type

### Step 3: Use the appropriate template

**API Reference Template:**

```mdx
---
title: Feature Name
description: Brief description of what this feature does.
---

{/* The content of this doc is shared between the app and pages router. You can use the `<PagesOnly>Content</PagesOnly>` component to add content that is specific to the Pages Router. Any shared content should not be wrapped in a component. */}

Brief introduction to the feature.

## Reference

### Props

<div style={{ overflowX: 'auto', width: '100%' }}>

| Prop                    | Example            | Type   | Status   |
| ----------------------- | ------------------ | ------ | -------- |
| [`propName`](#propname) | `propName="value"` | String | Required |

</div>

#### `propName`

Description of the prop.

\`\`\`tsx filename="app/example.tsx" switcher
// TypeScript example
\`\`\`

\`\`\`jsx filename="app/example.js" switcher
// JavaScript example
\`\`\`
```

**Guide Template:**

```mdx
---
title: How to do X in Next.js
nav_title: X
description: Learn how to implement X in your Next.js application.
---

Introduction explaining why this guide is useful.

## Prerequisites

What the reader needs to know before starting.

## Step 1: First Step

Explanation and code example.

\`\`\`tsx filename="app/example.tsx" switcher
// Code example
\`\`\`

## Step 2: Second Step

Continue with more steps...

## Next Steps

Related topics to explore.
```

### Step 4: Add related links

Update frontmatter with related documentation:

```yaml
related:
  title: Next Steps
  description: Learn more about related features.
  links:
    - app/api-reference/functions/related-function
    - app/guides/related-guide
```

## Documentation Conventions

See `references/DOC-CONVENTIONS.md` for complete formatting rules.

### Quick Reference

**Frontmatter (required):**

```yaml
---
title: Page Title (2-3 words)
description: One or two sentences describing the page.
---
```

**Code blocks:**

```
\`\`\`tsx filename="app/page.tsx" switcher
// TypeScript first
\`\`\`

\`\`\`jsx filename="app/page.js" switcher
// JavaScript second
\`\`\`
```

**Router-specific content:**

```mdx
<AppOnly>Content only for App Router docs.</AppOnly>

<PagesOnly>Content only for Pages Router docs.</PagesOnly>
```

**Notes:**

```mdx
> **Good to know**: Single line note.

> **Good to know**:
>
> - Multi-line note point 1
> - Multi-line note point 2
```

## Validation Checklist

Before committing documentation changes:

- [ ] Frontmatter has `title` and `description`
- [ ] Code blocks have `filename` attribute
- [ ] TypeScript examples use `switcher` with JS variant
- [ ] Props tables are properly formatted
- [ ] Related links point to valid paths
- [ ] `pnpm lint` passes
- [ ] Changes render correctly (if preview available)

## References

- `references/DOC-CONVENTIONS.md` - Complete frontmatter and formatting rules
- `references/CODE-TO-DOCS-MAPPING.md` - Source code to documentation mapping

<!-- chapter:end slug=update-docs -->

---

<!-- chapter:begin slug=v8-jit position=12 -->

## 12. v8-jit

- **Source:** https://github.com/vercel/next.js/blob/canary/.agents/skills/v8-jit/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/v8-jit/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/v8-jit.md
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: v8-jit
description: >
  V8 JIT optimization patterns for writing high-performance JavaScript in
  Next.js server internals. Use when writing or reviewing hot-path code in
  app-render, stream-utils, routing, caching, or any per-request code path.
  Covers hidden classes / shapes, monomorphic call sites, inline caches,
  megamorphic deopt, closure allocation, array packing, and profiling with
  --trace-opt / --trace-deopt.
user-invocable: false
metadata:
  internal: true
---

# V8 JIT Optimization

Use this skill when writing or optimizing performance-critical code paths in
Next.js server internals — especially per-request hot paths like rendering,
streaming, routing, and caching.

## Background: V8's Tiered Compilation

V8 compiles JavaScript through multiple tiers:

1. **Ignition** (interpreter) — executes bytecode immediately.
2. **Sparkplug** — fast baseline compiler (no optimization).
3. **Maglev** — mid-tier optimizing compiler.
4. **Turbofan** — full optimizing compiler (speculative, type-feedback-driven).

Code starts in Ignition and is promoted to higher tiers based on execution
frequency and collected type feedback. Turbofan produces the fastest machine
code but **bails out (deopts)** when assumptions are violated at runtime.

The key principle: **help V8 make correct speculative assumptions by keeping
types, shapes, and control flow predictable.**

## Hidden Classes (Shapes / Maps)

Every JavaScript object has an internal "hidden class" (V8 calls it a _Map_,
the spec calls it a _Shape_). Objects that share the same property names, added
in the same order, share the same hidden class. This enables fast property
access via inline caches.

### Initialize All Properties in Constructors

```ts
// GOOD — consistent shape, single hidden class transition chain
class RequestContext {
  url: string
  method: string
  headers: Record<string, string>
  startTime: number
  cached: boolean

  constructor(url: string, method: string, headers: Record<string, string>) {
    this.url = url
    this.method = method
    this.headers = headers
    this.startTime = performance.now()
    this.cached = false // always initialize, even defaults
  }
}
```

```ts
// BAD — conditional property addition creates multiple hidden classes
class RequestContext {
  constructor(url, method, headers, options) {
    this.url = url
    this.method = method
    if (options.timing) {
      this.startTime = performance.now() // shape fork!
    }
    if (options.cache) {
      this.cached = false // another shape fork!
    }
    this.headers = headers
  }
}
```

**Rules:**

- Assign every property in the constructor, in the same order, for every
  instance. Use `null` / `undefined` / `false` as default values rather than
  omitting the property.
- Prefer factory functions when constructing hot-path objects. A single factory
  makes it harder to accidentally fork shapes in different call sites.
- Never `delete` a property on a hot object — it forces a transition to
  dictionary mode (slow properties).
- Avoid adding properties after construction (`obj.newProp = x`) on objects
  used in hot paths.
- Object literals that flow into the same function should have keys in the
  same order:
- Use tuples for very small fixed-size records when names are not needed.
  Tuples avoid key-order pitfalls entirely.

```ts
// GOOD — same key order, shares hidden class
const a = { type: 'static', value: 1 }
const b = { type: 'dynamic', value: 2 }

// BAD — different key order, different hidden classes
const a = { type: 'static', value: 1 }
const b = { value: 2, type: 'dynamic' }
```

### Real Codebase Example

`Span` in `src/trace/trace.ts` initializes all fields in the constructor in a
fixed order — `name`, `parentId`, `attrs`, `status`, `id`, `_start`, `now`.
This ensures all `Span` instances share one hidden class.

## Monomorphic vs Polymorphic vs Megamorphic

V8's inline caches (ICs) track the types/shapes seen at each call site or
property access:

| IC State        | Shapes Seen | Speed                                 |
| --------------- | ----------- | ------------------------------------- |
| **Monomorphic** | 1           | Fastest — single direct check         |
| **Polymorphic** | 2–4         | Fast — linear search through cases    |
| **Megamorphic** | 5+          | Slow — hash-table lookup, no inlining |

Once an IC goes megamorphic it does NOT recover (until the function is
re-compiled). Megamorphic ICs also **prevent Turbofan from inlining** the
function.

### Keep Hot Call Sites Monomorphic

```ts
// GOOD — always called with the same argument shape
function processChunk(chunk: Uint8Array): void {
  // chunk is always Uint8Array → monomorphic
}

// BAD — called with different types at the same call site
function processChunk(chunk: Uint8Array | Buffer | string): void {
  // IC becomes polymorphic/megamorphic
}
```

**Practical strategies:**

- Normalize inputs at the boundary (e.g. convert `Buffer` → `Uint8Array`
  once) and keep internal functions monomorphic.
- Avoid passing both `null` and `undefined` for the same parameter — pick one
  sentinel value.
- When a function must handle multiple types, split into separate specialized
  functions and dispatch once at the entry point:

```ts
// Entry point dispatches once
function handleStream(stream: ReadableStream | Readable) {
  if (stream instanceof ReadableStream) {
    return handleWebStream(stream) // monomorphic call
  }
  return handleNodeStream(stream) // monomorphic call
}
```

This is the pattern used in `stream-ops.ts` and throughout the stream-utils
code (Node.js vs Web stream split via compile-time switcher).

## Closure and Allocation Pressure

Every closure captures its enclosing scope. Creating closures in hot loops
or per-request paths generates GC pressure and can prevent escape analysis.

### Hoist Closures Out of Hot Paths

```ts
// BAD — closure allocated for every request
function handleRequest(req) {
  stream.on('data', (chunk) => processChunk(chunk, req.id))
}

// GOOD — shared listener, request context looked up by stream
const requestIdByStream = new WeakMap()
function onData(chunk) {
  const id = requestIdByStream.get(this)
  if (id !== undefined) processChunk(chunk, id)
}

function processChunk(chunk, id) {
  /* ... */
}

function handleRequest(req) {
  requestIdByStream.set(stream, req.id)
  stream.on('data', onData)
}
```

```ts
// BEST — pre-allocate the callback as a method on a context object
class StreamProcessor {
  id: string
  constructor(id: string) {
    this.id = id
  }
  handleChunk(chunk: Uint8Array) {
    processChunk(chunk, this.id)
  }
}
```

### Avoid Allocations in Tight Loops

```ts
// BAD — allocates a new object per iteration
for (const item of items) {
  doSomething({ key: item.key, value: item.value })
}

// GOOD — reuse a mutable scratch object
const scratch = { key: '', value: '' }
for (const item of items) {
  scratch.key = item.key
  scratch.value = item.value
  doSomething(scratch)
}
```

### Real Codebase Example

`node-stream-helpers.ts` hoists `encoder`, `BUFFER_TAGS`, and tag constants to
module scope to avoid re-creating them on every request. The `bufferIndexOf`
helper uses `Buffer.indexOf` (C++ native) instead of a per-call JS loop,
eliminating per-chunk allocation.

## Array Optimizations

V8 tracks array "element kinds" — an internal type tag that determines how
elements are stored in memory:

| Element Kind      | Description                   | Speed                       |
| ----------------- | ----------------------------- | --------------------------- |
| `PACKED_SMI`      | Small integers only, no holes | Fastest                     |
| `PACKED_DOUBLE`   | Numbers only, no holes        | Fast                        |
| `PACKED_ELEMENTS` | Mixed/objects, no holes       | Moderate                    |
| `HOLEY_*`         | Any of above with holes       | Slower (extra bounds check) |

**Transitions are one-way** — once an array becomes `HOLEY` or `PACKED_ELEMENTS`,
it never goes back.

### Rules

- Pre-allocate arrays with known size: `new Array(n)` creates a holey array.
  Prefer `[]` and `push()`, or use `Array.from({ length: n }, initFn)`.
- Don't create holes: `arr[100] = x` on an empty array creates 100 holes.
- Don't mix types: `[1, 'two', {}]` immediately becomes `PACKED_ELEMENTS`.
- Prefer typed arrays only when you need binary interop/contiguous memory or
  have profiling evidence that they help. For small/short-lived collections,
  normal arrays can be faster and allocate less.

```ts
// GOOD — packed SMI array
const indices: number[] = []
for (let i = 0; i < n; i++) {
  indices.push(i)
}

// BAD — holey from the start
const indices = new Array(n)
for (let i = 0; i < n; i++) {
  indices[i] = i
}
```

### Real Codebase Example

`accumulateStreamChunks` in `app-render.tsx` uses `const staticChunks: Array<Uint8Array> = []` with `push()` — keeping a packed array of a single type
throughout its lifetime.

## Function Optimization and Deopts

### Hot-Path Deopt Footguns

- **`arguments` object**: using `arguments` in non-trivial ways (e.g.
  `arguments[i]` with variable `i`, leaking `arguments`). Use rest params
  instead.
- **Type instability at one call site**: same operation sees both numbers and
  strings (or many object shapes) and becomes polymorphic/megamorphic.
- **`eval` / `with`**: prevents optimization entirely.
- **Highly dynamic object iteration**: avoid `for...in` on hot objects; prefer
  `Object.keys()` / `Object.entries()` when possible.

### Favor Predictable Control Flow

```ts
// GOOD — predictable: always returns same type
function getStatus(code: number): string {
  if (code === 200) return 'ok'
  if (code === 404) return 'not found'
  return 'error'
}

// BAD — returns different types
function getStatus(code: number): string | null | undefined {
  if (code === 200) return 'ok'
  if (code === 404) return null
  // implicitly returns undefined
}
```

### Watch Shape Diversity in `switch` Dispatch

```ts
// WATCH OUT — `node.type` IC can go megamorphic if many shapes hit one site
function render(node) {
  switch (node.type) {
    case 'div':
      return { tag: 'div', children: node.children }
    case 'span':
      return { tag: 'span', text: node.text }
    case 'img':
      return { src: node.src, alt: node.alt }
    // Many distinct node layouts can make this dispatch site polymorphic
  }
}
```

This pattern is not always bad. Often the main pressure is at the shared
dispatch site (`node.type`), while properties used only in one branch stay
monomorphic within that branch. Reach for normalization/splitting only when
profiles show this site is hot and polymorphic.

## String Operations

- **String concatenation in loops is usually fine in modern V8** (ropes make
  many concatenations cheap). For binary data, use `Buffer.concat()`.
- **Template literals vs concatenation**: equivalent performance in modern V8,
  but template literals are clearer.
- **`string.indexOf()` > regex** for simple substring checks.
- **Reuse RegExp objects**: don't create a `new RegExp()` inside a hot
  function — hoist it to module scope.

```ts
// GOOD — regex hoisted to module scope
const ROUTE_PATTERN = /^\/api\//

function isApiRoute(path: string): boolean {
  return ROUTE_PATTERN.test(path)
}

// BAD — regex recreated on every call
function isApiRoute(path: string): boolean {
  return /^\/api\//.test(path) // V8 may or may not cache this
}
```

## `Map` and `Set` vs Plain Objects

- **`Map`** is faster than plain objects for frequent additions/deletions
  (avoids hidden class transitions and dictionary mode).
- **`Set`** is faster than `obj[key] = true` for membership checks with
  dynamic keys.
- For **static lookups** (known keys at module load), plain objects or
  `Object.freeze({...})` are fine — V8 optimizes them as constant.
- Never use an object as a map if keys come from user input (prototype
  pollution risk + megamorphic shapes).

## Profiling and Verification

### V8 Flags for Diagnosing JIT Issues

```bash
# Trace which functions get optimized
node --trace-opt server.js 2>&1 | grep "my-function-name"

# Trace deoptimizations (critical for finding perf regressions)
node --trace-deopt server.js 2>&1 | grep "my-function-name"

# Combined: see the full opt/deopt lifecycle
node --trace-opt --trace-deopt server.js 2>&1 | tee /tmp/v8-trace.log

# Show IC state transitions (verbose)
node --trace-ic server.js 2>&1 | tee /tmp/ic-trace.log

# Print optimized code (advanced)
node --print-opt-code --code-comments server.js
```

### Targeted Profiling in Next.js

```bash
# Profile a production build
node --cpu-prof --cpu-prof-dir=/tmp/profiles \
  node_modules/.bin/next build

# Profile the server during a benchmark
node --cpu-prof --cpu-prof-dir=/tmp/profiles \
  node_modules/.bin/next start &
# ... run benchmark ...
# Analyze in Chrome DevTools: chrome://inspect → Open dedicated DevTools

# Quick trace-deopt check on a specific test
node --trace-deopt $(which jest) --runInBand test/path/to/test.ts \
  2>&1 | grep -i "deopt" | head -50
```

### Using `%` Natives (Development/Testing Only)

With `--allow-natives-syntax`:

```js
function hotFunction(x) {
  return x + 1
}

// Force optimization
%PrepareFunctionForOptimization(hotFunction)
hotFunction(1)
hotFunction(2) % OptimizeFunctionOnNextCall(hotFunction)
hotFunction(3)

// Check optimization status
// 1 = optimized, 2 = not optimized, 3 = always optimized, 6 = maglev
console.log(%GetOptimizationStatus(hotFunction))
```

## Checklist for Hot Path Code Reviews

- [ ] All object properties initialized in constructor/literal, same order
- [ ] No `delete` on hot objects
- [ ] No post-construction property additions on hot objects
- [ ] Functions receive consistent types (monomorphic call sites)
- [ ] Type dispatch happens at boundaries, not deep in hot loops
- [ ] No closures allocated inside tight loops
- [ ] Module-scope constants for regex, encoders, tag buffers
- [ ] Arrays are packed (no holes, no mixed types)
- [ ] `Map`/`Set` used for dynamic key collections
- [ ] No `arguments` object — use rest params
- [ ] `try/catch` at function boundary, not inside tight loops
- [ ] String building via array + `join()` or `Buffer.concat()`
- [ ] Return types are consistent (no `string | null | undefined` mixes)

## Related Skills

- `$dce-edge` — DCE-safe require patterns (compile-time dead code)
- `$runtime-debug` — runtime bundle debugging and profiling workflow

<!-- chapter:end slug=v8-jit -->

---

<!-- chapter:begin slug=write-api-reference position=13 -->

## 13. write-api-reference

- **Source:** https://github.com/vercel/next.js/blob/canary/.agents/skills/write-api-reference/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/write-api-reference/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/write-api-reference.md
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: write-api-reference
description: |
  Produces API reference documentation for Next.js APIs: functions, components, file conventions, directives, and config options.

  **Auto-activation:** User asks to write, create, or draft an API reference page. Also triggers on paths like `docs/01-app/03-api-reference/`, or keywords like "API reference", "props", "parameters", "returns", "signature".

  **Input sources:** Next.js source code, existing API reference pages, or user-provided specifications.

  **Output type:** A markdown (.mdx) API reference page with YAML frontmatter, usage example, reference section, behavior notes, and examples.
agent: Plan
context: fork
metadata:
  internal: true
---

# Writing API Reference Pages

## Goal

Produce an API reference page that documents a single API surface (function, component, file convention, directive, or config option). The page should be concise, scannable, and example-driven.

Each page documents **one API**. If the API has sub-methods (like `cookies.set()`), document them on the same page. If two APIs are independent, they get separate pages.

## Structure

Identify which category the API belongs to, then follow the corresponding template.

### Categories

1. **Function** (`cookies`, `fetch`, `generateStaticParams`): signature, params/returns, methods table, examples
2. **Component** (`Link`, `Image`, `Script`): props summary table, individual prop docs, examples
3. **File convention** (`page`, `layout`, `route`): definition, code showing the convention, props, behavior, examples
4. **Directive** (`use client`, `use cache`): definition, usage, serialization/boundary rules, reference
5. **Config option** (`basePath`, `images`, etc.): definition, config code, behavioral sections

### Template

````markdown
---
title: {API name}
description: {API Reference for the {API name} {function|component|file convention|directive|config option}.}
---

{One sentence defining what it does and where it's used.}

```tsx filename="path/to/file.tsx" switcher
// Minimal working usage
```

```jsx filename="path/to/file.js" switcher
// Same example in JS
```

## Reference

{For functions: methods/params table, return type.}
{For components: props summary table, then `#### propName` subsections.}
{For file conventions: `### Props` with `#### propName` subsections.}
{For directives: usage rules and serialization constraints.}
{For config: options table or individual option docs.}

### {Subsection name}

{Description + code example + table of values where applicable.}

## Good to know

- {Default behavior or implicit effects.}
- {Caveats, limitations, or version-specific notes.}
- {Edge cases the developer should be aware of.}

## Examples

### {Example name}

{Brief context, 1-2 sentences.}

```tsx filename="path/to/file.tsx" switcher
// Complete working example
```

```jsx filename="path/to/file.js" switcher
// Same example in JS
```

## Version History

| Version  | Changes         |
| -------- | --------------- |
| `vX.Y.Z` | {What changed.} |
````

**Category-specific notes:**

- **Functions**: Lead with the function signature and `await` if async. Document methods in a table if the return value has methods (like `cookies`). Document options in a separate table if applicable.
- **Components**: Start with a props summary table (`| Prop | Example | Type | Required |`). Then document each prop under `#### propName` with description, code example, and value table where useful.
- **File conventions**: Show the default export signature with TypeScript types. Document each prop (`params`, `searchParams`, etc.) under `#### propName` with a route/URL/value example table.
- **Directives**: No `## Reference` section. Use `## Usage` instead, showing correct placement. Document serialization constraints and boundary rules.
- **Config options**: Show the `next.config.ts` snippet. Use subsections for each behavioral aspect.

## Rules

1. **Lead with what it does.** First sentence defines the API. No preamble.
2. **Show working code immediately.** A minimal usage example appears right after the opening sentence, before `## Reference`.
3. **Use `switcher` for tsx/jsx pairs.** Always include both. Always include `filename="path/to/file.ext"`.
4. **Use `highlight={n}` for key lines.** Highlight the line that demonstrates the API being documented.
5. **Tables for simple APIs, subsections for complex ones.** If a prop/param needs only a type and one-line description, use a table row. If it needs a code example or multiple values, use a `####` subsection.
6. **Behavior section uses `> **Good to know**:`or`## Good to know`.** Use the blockquote format for brief notes (1-3 bullets). Use the heading format for longer sections. Not "Note:" or "Warning:".
7. **Examples section uses `### Example Name` subsections.** Each example solves one specific use case.
8. **Version History table at the end.** Include when the API has changed across versions. Omit for new APIs.
9. **No em dashes.** Use periods, commas, or parentheses instead.
10. **Mechanical, observable language.** Describe what happens, not how it feels. "Returns an object" not "gives you an object".
11. **Link to related docs with relative paths.** Use `/docs/app/...` format.
12. **No selling or justifying.** No "powerful", "easily", "simply". State what the API does.

| Don't                                                   | Do                                                                                          |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| "This powerful function lets you easily manage cookies" | "`cookies` is an async function that reads HTTP request cookies in Server Components"       |
| "You can conveniently access..."                        | "Returns an object containing..."                                                           |
| "The best way to handle navigation"                     | "`<Link>` extends the HTML `<a>` element to provide prefetching and client-side navigation" |

13. **Bridge new framework terms with legacy or generic vocabulary.** When the API renames or differentiates from a prior concept (Pages-era term, generic web term, REST vocabulary), include one such synonym in the frontmatter `description` and once in prose. Example: `description: "Use Dynamic Segments to read URL parameters and generate routes from dynamic data."` mentions "URL parameters" alongside "Dynamic Segments". One synonym, folded into natural prose. No separate "Synonyms" or "Also known as" section, no keyword stuffing. Goal: preserve discoverability for users still searching the old vocabulary even when the framework has moved on.

| Don't                              | Do                                                                                   |
| ---------------------------------- | ------------------------------------------------------------------------------------ |
| "Learn how to use Route Handlers"  | "Build API endpoints with Route Handlers, the App Router replacement for API Routes" |
| "Configure dynamic route segments" | "Read URL parameters from dynamic route segments"                                    |

## Workflow

1. **Ask for reference material.** Ask the user if they have any RFCs, PRs, design docs, or other context that should inform the doc.
2. **Identify the API category** (function, component, file convention, directive, config).
3. **Research the implementation.** Read the source code to understand params, return types, edge cases, and defaults.
4. **Check e2e tests.** Search `test/` for tests exercising the API to find real usage patterns, edge cases, and expected behavior.
5. **Check existing related docs** for linking opportunities and to avoid duplication.
6. **Write using the appropriate category template.** Follow the rules above.
7. **Review against the rules.** Verify: one sentence opener, immediate code example, correct `switcher`/`filename` usage, tables vs subsections, "Good to know" format, no em dashes, mechanical language.

## References

Read these pages in `docs/01-app/03-api-reference/` before writing. They demonstrate the patterns above.

- `04-functions/cookies.mdx` - Function with methods table, options table, and behavior notes
- `03-file-conventions/page.mdx` - File convention with props subsections and route/URL/value tables
- `02-components/link.mdx` - Component with props summary table and detailed per-prop docs
- `01-directives/use-client.mdx` - Directive with usage section and serialization rules
- `04-functions/fetch.mdx` - Function with troubleshooting section and version history

<!-- chapter:end slug=write-api-reference -->

---

<!-- chapter:begin slug=write-guide position=14 -->

## 14. write-guide

- **Source:** https://github.com/vercel/next.js/blob/canary/.agents/skills/write-guide/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/write-guide/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/write-guide.md
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: write-guide
description: |
  Generates technical guides that teach real-world use cases through progressive examples.

  **Auto-activation:** User asks to write, create, or draft a guide or tutorial. Also use when converting feature documentation, API references, or skill knowledge into step-by-step learning content.

  **Input sources:** Feature skills, API documentation, existing code examples, or user-provided specifications.

  **Output type:** A markdown guide with YAML frontmatter, introduction, 2-4 progressive steps, and next steps section.
agent: Plan
context: fork
metadata:
  internal: true
---

# Writing Guides

## Goal

Produce a technical guide that teaches a real-world use case through progressive examples. Concepts are introduced only when the reader needs them.

Each guide solves **one specific problem**. Not a category of problems. If the outline has 5+ steps or covers multiple approaches, split it.

## Structure

Every guide follows this arc: introduction, example setup, 2-5 progressive steps, next steps.

Each step follows this loop: working code → new requirement → friction → explanation → resolution → observable proof.

Sections: introduction (no heading, 2 paragraphs max), `## Example` (what we're building + source link), `### Step N` (action-oriented titles, 2-4 steps), `## Next steps` (summary + related links).

Headings should tell a story on their own. If readers only saw the headings, they'd understand the guide's takeaway.

### Template

````markdown
---
title: {Action-oriented, e.g., "Building X" or "How to Y"}
description: {One sentence}
nav_title: {Short title for navigation}
---

{What the reader will accomplish and why it matters. The friction and how this approach resolves it. 2 paragraphs max.}

## Example

As an example, we'll build {what we're building}.

We'll start with {step 1}, then {step 2}, and {step 3}.

{Source code link.}

### Step 1: {Action-oriented title}

{Brief context, 1-2 sentences.}

```tsx filename="path/to/file.tsx"
// Minimal working code
```

{Explain what happens.}

{Introduce friction: warning, limitation, or constraint.}

{Resolution: explain the choice, apply the fix.}

{Verify the fix with observable proof.}

### Step 2: {Action-oriented title}

{Same pattern: context → code → explain → friction → resolution → proof.}

### Step 3: {Action-oriented title}

{Same pattern.}

## Next steps

You now know how to {summary}.

Next, learn how to:

- [Related guide 1]()
- [Related guide 2]()
````

### Workflow

1. **Research**: Check available skills for relevant features. Read existing docs for context and linking opportunities.
2. **Plan**: Outline sections. Verify scope (one problem, 2-4 steps). Each step needs a friction point and resolution.
3. **Write**: Follow the template above. Apply the rules below.
4. **Review**: Re-read the rules, verify, then present.

## Rules

1. **Progressive disclosure.** Start with the smallest working example. Introduce complexity only when the example breaks. Name concepts at the moment of resolution, after the reader has felt the problem. Full loop: working → new requirement → something breaks → explain why → name the fix → apply → verify with proof → move on.
2. **Show problems visually.** Console errors, terminal output, build warnings, slow-loading pages. "If we refresh the page, we can see the component blocks the response."
3. **Verify resolutions with observable proof.** Before/after comparisons, browser reloads, terminal output. "If we refresh the page again, we can see it loads instantly."
4. **One friction point per step.** If a step has multiple friction points, split it.
5. **Minimal code blocks.** Only the code needed for the current step. Collapse unchanged functions with `function Header() {}`.
6. **No em dashes.** Use periods, commas, or parentheses instead.
7. **Mechanical, observable language.** Describe what happens, not how it feels.
8. **No selling, justifying, or comparing.** No "the best way," no historical context, no framework comparisons.

| Don't                                                | Do                                                       |
| ---------------------------------------------------- | -------------------------------------------------------- |
| "creates friction in the pipeline"                   | "blocks the response"                                    |
| "needs dynamic information"                          | "depends on request-time data"                           |
| "requires dynamic processing"                        | "output can't be known ahead of time"                    |
| "The component blocks the response — causing delays" | "The component blocks the response. This causes delays." |

9. **Bridge new framework terms with legacy or generic vocabulary in `description` and intro.** Guides win or lose SERPs on the colloquial query (e.g. "next js form submission", "next js api endpoint", "next js error page"), not on the framework's preferred noun. When the guide covers a renamed or differentiated concept, include one synonym (Pages-era term, REST/web term, or industry-standard label) in the frontmatter `description` and once in the introduction. Fold into prose. No separate "Synonyms" or "Also known as" section.

| Don't                                            | Do                                                                                                            |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
| "Learn how to use Route Handlers"                | "Build API endpoints (formerly API Routes) with Route Handlers"                                               |
| "Learn how to mutate data with Server Functions" | "Submit forms and update data with Server Functions, the App Router approach to form posts and API mutations" |

## References

Read these guides in `docs/01-app/02-guides/` before writing. They demonstrate the patterns above.

- `public-static-pages.mdx` — intro → example → 3 progressive steps → next steps. Concepts named at point of resolution. Problems shown with build output.
- `forms.mdx` — progressive feature building without explicit "Step" labels. Each section adds one capability.

<!-- chapter:end slug=write-guide -->

---

<!-- chapter:begin slug=next-cache-components-adoption position=15 -->

## 15. next-cache-components-adoption

- **Source:** https://github.com/vercel/next.js/blob/canary/skills/next-cache-components-adoption/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/skills/next-cache-components-adoption/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/next-cache-components-adoption.md
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

Bundled files (2), referenced from this skill's directory:
  - `references/dev-only-validations.md` — https://raw.githubusercontent.com/vercel/next.js/canary/skills/next-cache-components-adoption/references/dev-only-validations.md
  - `references/per-page-decisions.md` — https://raw.githubusercontent.com/vercel/next.js/canary/skills/next-cache-components-adoption/references/per-page-decisions.md

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: next-cache-components-adoption
description: >
  Turn on Cache Components in a Next.js app and resolve the blocking routes it
  surfaces. Use when the user wants to enable, adopt, or migrate to Cache
  Components, flip the `cacheComponents` flag, work through a flood of
  blocking-prerender / instant validation errors, run the
  `cache-components-instant-false` codemod, or decide between opting routes out
  with `export const instant = false` and fixing them in place.
---

# next-cache-components-adoption

Enable Cache Components on an app and walk it to a passing build. This skill sequences the work; per-error recipes live in the dev overlay fix cards and the build's terminal output. The [migrating to Cache Components guide](https://nextjs.org/docs/app/guides/migrating-to-cache-components) is the canonical reference for the concepts and per-API recipes this skill applies — consult it whenever the skill steps reference a pattern (`"use cache"`, `cacheLife`, `<Suspense>` placement, etc.) and you want the full explanation.

## requires

- **App Router project.** Cache Components is an App Router feature; `cacheComponents: true` does nothing for `pages/` routes. If the project has a `pages/` or `src/pages/` tree but no `app/` or `src/app/` tree, stop and tell the user — Pages → App migration is its own project, not part of this skill. A hybrid app (both `pages/` and `app/`) is fine: the flag affects the `app/` routes; `pages/` routes are unaffected and don't need opt-outs.

- **A resolved app directory.** Locate `next.config.{js,ts,mjs,cjs}` first: that's the project root, and an agent invoked from a subdirectory would otherwise test for `app/` against the wrong `cwd` and find nothing. Look for `app/` and `src/app/` under it, and treat every command and glob in this skill as relative to whichever one exists. If both exist, Next.js builds `app/` and never looks at `src/app/`, so its routes are shadowed and unbuilt — tell the user that and ask which tree to migrate instead of picking one.

- **A runnable app.** The whole loop verifies against `next dev` and a browser, so the app has to boot. If it reads a database or required env at import (e.g. an `env.ts` that throws on a missing `DATABASE_URL`), confirm it actually starts — with the real environment, or local data you stand up — before step 1. Adoption can't be verified against an app that won't run.

- **Next.js 16.3 or later.** That release is where the pieces this skill relies on land: top-level `cacheComponents`, `export const instant`, the dev-overlay instant-navigation validation warnings, and the `cache-components-instant-false` codemod. If `next --version` reports below 16.3, upgrade first:
  - `npx @next/codemod@latest upgrade latest` to apply the version-to-version codemods.
  - Read the relevant [version upgrade guide](https://nextjs.org/docs/app/guides/upgrading) (e.g. [Version 16](https://nextjs.org/docs/app/guides/upgrading/version-16)) for what the codemod doesn't cover.

- **No incompatible config keys.** `cacheComponents: true` errors on any file that still exports `dynamic`, `revalidate`, or `fetchCache`. **Translate, don't delete.** Each export encodes behavior the route needs to keep doing; migrate each one to its Cache Components equivalent via the [migration guide's per-key sections](https://nextjs.org/docs/app/guides/migrating-to-cache-components#enable-cache-components). The exception is `dynamic = 'force-dynamic'`: under Cache Components every route is already dynamic by default, so the migration guide removes it outright rather than translating it — don't overthink a batch of identical `force-dynamic` deletions. `revalidate` and `fetchCache` still need real translation. If a value can't be cleanly translated yet, leave a `// TODO: Cache Components adoption — restore revalidate = 3600` comment so the loop picks it up. The `cache-components-instant-false` codemod does not touch these.

- **`experimental.dynamicIO` is fatal.** It was renamed to top-level `cacheComponents` and the old key now aborts before any build can run — remove it (or replace with `cacheComponents: true`) first. `experimental.useCache` is still accepted as a deprecated alias; redundant once `cacheComponents: true` is set, so remove it for clarity.

### notes

- **No passing baseline before the flag.** If the app already uses `"use cache"`, the pre-flag build errors with `please enable the feature flag cacheComponents`. Enabling the flag is the first thing you do (in Incremental, before the codemod; in Direct, before fixing routes) — not a thing to do _after_ getting a passing build. Note this in your starting summary so it doesn't read as a regression.

- **Offline docs.** Guide links have offline copies under `node_modules/next/dist/docs/` (bundled since Next.js 16.2), with the directory layout numbered for ordering (e.g. `node_modules/next/dist/docs/01-app/02-guides/migrating-to-cache-components.md`). If you can't predict the numbered prefix, `find node_modules/next/dist/docs -name '<slug>.md'` resolves it. The `/docs/messages/*` error pages are not bundled.

- **Older versions without bundled docs.** Suggest `npx @next/codemod@latest agents-md` to the user before starting: it downloads a version-matched copy to `.next-docs/` and writes an index into `AGENTS.md` / `CLAUDE.md`. It touches files in their repo, so ask first and run it only if they want it.

## the shape of the work

There's one loop: walk the route tree top-down, one feature at a time, adopting each route against `next dev` + a browser. The build is a final check for each feature, not the working surface.

The choice in step 1 is whether to opt every route out of validation first or fix routes as you go. Either way the loop is the same:

- **With a quiet pre-step (Incremental).** Run the codemod to opt every page and layout out of validation. Once you've also fixed what the codemod can't (sync-IO calls, leftover `revalidate`/`dynamic`/`fetchCache` exports), the build passes; you ship that as its own PR and then start the loop — removing one opt-out at a time and adopting that route. This splits the work into small, reviewable PRs.
- **Without (Direct).** Enable `cacheComponents` and start the loop on whatever the build flags first. Same loop, but every fix sits on one branch until adoption is complete.

In both, the per-route success bar is the same: **dev loop reports no errors AND `next build` passes**. Check in with the user after every feature, and suggest a commit but never make one without their confirmation. Expect to spend most of the time in the loop, not in the pre-step.

## background

`cacheComponents: true` requires every route to be prerenderable. A route that reads request-time data outside `<Suspense>` is "blocking" and fails the build. `export const instant = false` marks a route as allowed to block, which clears it in both dev and build; on a layout it covers the whole subtree during the build, but client navigations still validate each descendant segment on its own. Reads wrapped in a [`"use cache"`](https://nextjs.org/docs/app/api-reference/directives/use-cache) function count as cache boundaries, not blocking reads.

Three classes of blocker come up, usually in this order:

1. **Request-time reads** (`cookies()`, `headers()`, `await params`, `await searchParams`). All four block when awaited at the top of a page or layout. `params` and `searchParams` often get missed because they're not framed as "request data" the way cookies and headers are. The fix is to push the read into a `<Suspense>`-wrapped child — and for `params`/`searchParams`, forward the promise into the child and await it there; don't `await` at the page top.
2. **Sync-IO at module/render time** (`new Date()`, `Date.now()`, `Math.random()`, `crypto.randomUUID()`). These fail the build even with `instant = false` — the opt-out doesn't suppress them. If they're in a shared layout, they block every route under it. The codemod can't fix them; after running it, use the build errors to identify which calls to translate by hand (see the [incremental pre-step](#incremental)).
3. **`"use cache"` files that read request data.** A file with a top-level `"use cache"` directive can't export `instant`; combining the two errors with `Only async functions are allowed to be exported in a "use cache" file.`, which means the directive was wrong for that route. Remove it before running the codemod.

## working surfaces

### finding blocking routes

Prefer `next dev` over `next build` while you work.

- **`next dev`** — the working surface. Visit a route; its blocking errors surface in the dev overlay with full stack traces and fix cards linking the per-error docs. Work one route at a time — errors don't accumulate in one place. The route itself still returns HTTP 200, so read the overlay (or `.next-dev.log`), not status codes. A cleared overlay is one half of calling a route clean — the other half is browser verification (see [step 2](#step-2-the-inner-loop-remove-opt-outs-one-feature-at-a-time)) and a passing build for that route.
- **`next build`** — detection only. The build is `next dev`'s authoritative check, not its replacement. Use it as the last gate on each feature in the loop (a passing build is part of the per-route success bar) and as the final verification across the whole app. In Incremental, the build also confirms the pre-step (codemod opted every route out, no shared layout still has a sync-IO blocker) before you ship that PR. Don't reach for the build instead of the dev loop while you're working a route — a passing compile doesn't tell you what ended up in the static shell and what streamed. By default the build stops at the first blocking route, so it's also poor for sizing the work. Two flags help when iterating: `--debug-build-paths` builds only the routes you name (comma-separated glob patterns of file paths relative to the project root, e.g. `--debug-build-paths="app/admin/**/page.tsx"` — not URL paths; `--debug-build-paths="app/(marketing)/about/page.tsx"` — not `/about`; `--debug-build-paths="app/admin"` matches nothing and silently builds zero routes), and `--debug-prerender` disables the early exit so the build continues past the first prerender failure, reports every blocking route, and prints a fuller stack trace that names the originating file and line.

Every blocking error has a docs page — open it. Both the dev overlay and the build terminal print a `https://nextjs.org/docs/messages/<slug>` link with each error. That page is the canonical recipe for the fix; the inline message is a summary. Fetch the link for every distinct error you encounter, even if you think you know the pattern — the recipes evolve, and the same error class can have different correct fixes depending on what the route reads. Don't improvise from the inline message alone. (`/docs/messages/*` pages aren't bundled offline; if you have no network, fall back to the per-API guides under `node_modules/next/dist/docs/` and note the limitation when you report back.)

### verifying each fix at runtime

A passing build or a cleared overlay isn't proof the route actually behaves — Cache Components is a runtime concern (a static shell with streamed data). Verify after every fix, not only at the end.

In preference order:

1. **[`next-dev-loop`](https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop) — strongly preferred.** Cross-checks `/_next/mcp` against the live browser via `agent-browser` and surfaces both compile and runtime issues in one pass. The diagnostics (React tree, suspense boundaries, console + network) are richer than poking at `next dev` by hand.

   Install it before starting the loop. Don't wait until you hit something `next dev` alone can't explain. It ships alongside this skill, so check whether it is already available first, and install it only if it is not:

   ```bash
   npx skills add https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop
   ```

   The skill states its required `agent-browser` version and walks you through it.

   **Requires Turbopack.** If `package.json`'s `dev` script passes `--webpack`, flag it to the user and ask whether there's a reason to stay on webpack. If not, switch to Turbopack (the Next.js 16.3+ default). If they want to keep webpack, skip this install and use the [build-only loop](#the-loop-build-only-fallback) instead.

   You don't need permission to install `next-dev-loop` itself. It's a tool, like installing a dev dependency. If a user is present, briefly tell them you're installing it for verification. In a non-interactive run (CI, dashboard, sandbox), install it without asking — "can't prompt the user" is not a reason to skip. The only legitimate skip is a real technical blocker: no network, no npm, read-only filesystem, a stated no-new-deps policy, or a webpack-only dev script. If you skip, name the specific blocker in your final report.

2. **A browser you can drive yourself.** Playwright, `agent-browser` directly, any browser-automation tool. Use only when `next-dev-loop` is genuinely blocked. You'll miss the framework-side checks (`/_next/mcp`), so DOM assertions alone don't catch every regression — be more cautious about what you call "verified."

3. **Build-only.** If you can't run a dev server at all, the build is your only signal. `○ (Static)` routes with no `<Suspense>` are fully verified by the build (nothing streamed to test). `◐ (Partial Prerender)` routes are only shell-verified — flag them when you report back.

4. **No tooling at all.** Ask the user to run the dev server (or build) and report what they see, or hand off the milestone you've reached.

## step 1: choose a strategy

Ask the user, in terms of the PRs they want, not the size of the job. Never use the internal labels (Incremental, Direct) when talking to the user — those are your own scaffolding. Ask in terms of PRs and features, e.g.: _"Do you want me to first open a PR that turns on Cache Components and opts every route out of validation, then handle the actual route adoptions feature-by-feature in follow-up PRs? Or do everything on one branch?"_ Even on a tiny app, the incremental path still has value (review-sized PR, revertible, the `// TODO: Cache Components adoption` markers double as your work queue for next session). Don't pick on their behalf.

If there's no user to ask, default to **Incremental** and document the choice.

- **Incremental** — quiet pre-step + the loop. Run the codemod to opt every page and layout out of validation, get the build passing, stop and check in with the user (see [end of the pre-step](#end-of-the-pre-step-check-in)), then enter [step 2's loop](#step-2-the-inner-loop-remove-opt-outs-one-feature-at-a-time) and ship each feature as a follow-up PR.
- **Direct** — skip the pre-step. Enable `cacheComponents` and go straight to [step 2's loop](#step-2-the-inner-loop-remove-opt-outs-one-feature-at-a-time); the build's blocking routes are the work queue.

### incremental

Before invoking the codemod, fix the blocker that does not require build feedback.

1. **Incompatible segment configs.** Grep for `^export const (revalidate|dynamic|fetchCache)` across the app directory and translate per the `requires` note above. The codemod does not touch them; leaving them in place fails the build after the codemod.

The codemod refuses to run on a dirty working tree. Commit or stash unrelated work first, or pass `--force` to let its edits land alongside your WIP. Common false positive: if you recently upgraded Next.js, `package.json` and the lockfile will already be dirty — commit those first.

```bash
npx @next/codemod@latest cache-components-instant-false ./app
```

Pass the app directory you resolved in [requires](#requires). A wrong path is not an error: it reports `0 ok` and exits `0`, so read the file count and treat zero as a failed run, not an adopted app.

Inserts `export const instant = false` (with a `// TODO: Cache Components adoption` comment) into every `{page,layout,default}` file under that directory, skipping files that already declare `instant` and any module marked `"use client"` or `"use server"`. Then set `cacheComponents: true`. The TODO comments are the work queue for the loop.

If the codemod isn't available (older `@next/codemod`, sandboxed environment, offline run), reproduce it by hand: for every `{page,layout,default}.{js,jsx,ts,tsx}` in the app directory that isn't `"use client"` or `"use server"` and doesn't already declare `instant`, insert this after the imports:

```ts
// TODO: Cache Components adoption. Refactor this route so this opt-out can be removed.
// See: https://nextjs.org/docs/app/guides/migrating-to-cache-components
export const instant = false
```

The codemod opts every segment out, not only the root, on purpose. Resolution is top-down, first-explicit-config-wins: the highest `instant = false` decides the whole subtree. With an opt-out on every segment, removing one segment's opt-out validates only that segment; descendants keep their own opt-outs and stay passing. If only the root were opted out, removing it would re-arm validation for the entire app at once.

Because the highest opt-out wins, remove them top-down (root layout first, then descend). Removing a leaf's opt-out does nothing while an ancestor still holds one.

Next, run `next build` to surface blockers the codemod could not handle. The build is the proof, not the codemod run — a shared layout that calls `new Date()` / `Math.random()` directly still fails regardless of the opt-out (see [background](#background)). If the normal build reports a sync-IO error without locating the call, rerun that route with `next build --debug-prerender --debug-build-paths="app/path/to/page.tsx"`. For each sync-IO error it reports:

1. **Sync-IO at module/render time.** Use the route, originating file and line, and `/docs/messages/` link in the build output to locate the error. If needed, grep the whole repo for `new Date()`, `Date.now()`, `Math.random()`, and `crypto.randomUUID()` (not only `app/**/layout.{js,jsx,ts,tsx}` — the read might live in any component imported by a layout). Do not change unreported matches. Unblock the reported call with the `await connection()` + `<Suspense>` fix from its `blocking-prerender-*` error card: it defers the value to request time, exactly as it behaved before the migration, so it needs no product decision. Add this exact comment on the line above the `await connection()`:

   ```tsx
   // TODO: Cache Components adoption. Added to unblock the build: remove this connection() to re-trigger the error and review the fix options.
   ```

   It shares the `TODO: Cache Components adoption` prefix with the comments the codemod writes, so the check-in grep finds both. Removing the `await connection()` makes the error fire again with its fix cards — the same motion as removing an opt-out in the loop.

After each fix, rerun the scoped build when available, then run `next build` again to find the next blocker. Repeat until the normal build passes.

After the build passes, confirm the root layout got an opt-out (`grep -n "export const instant" <app dir>/layout.*`). The root layout renders every route, including framework routes like `/_not-found`, so if it was missed, add `export const instant = false` to it by hand.

Synthetic routes like `/_not-found` have no user file — when they block, fix the root layout's opt-out, not the synthetic route. Client Components (`"use client"`) get no opt-out (it's a build error to export `instant` from them), but they are not a rare blocker. The high-frequency case is a client component in the root layout's nav or header calling `usePathname()`/`useSearchParams()`: it blocks _every_ dynamic route with `blocking-prerender-client-hook`, and static routes pass (the pathname is known at prerender), which masks it until you reach a dynamic segment. It's not an ancestor-data fix — follow the [error's docs page](https://nextjs.org/docs/messages/blocking-prerender-client-hook) for the `<Suspense>` recipe. Only when a client route blocks on _server_ data do you fix that data in its ancestor.

### end of the pre-step: check in

Incremental only. Stop here before starting step 2 — the pre-step is the shippable PR. Talk to the user in their language; don't say "Incremental" or other internal labels; talk about adoption, PRs, and what the app does now. Tell them:

- What you did: turned on Cache Components, ran the codemod that opts every page and layout out of the new validation (or did it by hand), fixed any blockers the codemod can't (list them), confirmed the build passes.
- What changed: every page and layout in the app directory now exports `instant = false` with a `// TODO: Cache Components adoption` comment, except client components and any that already had an `instant` export.
- What to sanity-check: the diff is mostly mechanical (new exports + comments). The build passes. Routes still behave exactly as they did before — the opt-outs preserve current behavior; no rendering changes yet.
- The question: "Want to open this as its own PR before we start adopting Cache Components route by route? Or keep going on this branch?" Wait for the answer.

Moving to step 2 without checking in defeats the point of taking the incremental path.

### direct

Set `cacheComponents: true` and move to [step 2](#step-2-the-inner-loop-remove-opt-outs-one-feature-at-a-time). The build's blocking routes are the work queue.

## step 2: the inner loop, remove opt-outs one feature at a time

A "feature" is a single product surface — `app/settings/profile/**`, `app/posts/[slug]/**` — not a whole top-level app like `app/dashboard/**`. Finish one end-to-end before starting the next.

Within a feature, walk top-down (layouts before pages, root layout first). Removing a layout's opt-out before its descendants exposes the layout's own blocking reads. (Direct: there are no opt-outs to remove — fix each failing route; if a hand-written opt-out on an ancestor shadows it, remove that first.)

A passing build mid-walk doesn't mean the layout is clean. Removing a layout's opt-out while its descendant pages still have theirs keeps the build passing — each page shadows the inherited validation. The layout's actual blocking reads only surface once nothing below it shadows them. Don't call a feature done at the layout boundary.

Use the **with-a-browser** loop unless a browser is genuinely unreachable. The [`next-dev-loop`](#verifying-each-fix-at-runtime) skill is the source of truth for what counts as "browser available" and how to install it.

### the loop, with a browser (preferred)

Per route:

- Remove the opt-out (Incremental) or target the failing route (Direct).
- Reload in dev. Overlay clean? Skip to verify. Overlay still red? Fix.
- Fix — fetch the docs page linked from the error (`https://nextjs.org/docs/messages/<slug>`), apply the recipe from there. The inline overlay text is a summary; the docs page is the source of truth.
- Verify in the browser. Confirm the visible content on first paint is what you intended in the shell — not stuck on a fallback, not silently streaming everything out of an empty shell.
- Re-check siblings if the fix touched shared code (a layout, a sidebar component). A shared-shell change can fix the route you're on and break a sibling.

### the loop, build-only (fallback)

Used when there's no way to drive a browser — CI, sandbox, the user has no `next dev` running and you can't start one. Weaker signal: confirms the build passes and the route prerenders, but not what ended up in the static shell vs streamed.

Per route:

- Remove the opt-out (Incremental) or target the failing route (Direct).
- Rebuild with `--debug-build-paths app/<route>/**` (only that route) or `--debug-prerender` (full build, but past the first failure). Route passing? Move on. Still blocking? Fix.
- Fix — fetch the docs page linked from the error (`https://nextjs.org/docs/messages/<slug>`), apply the recipe from there.
- Re-check siblings if the fix touched shared code.
- Flag the route as build-only-verified when you hand the feature off. Each `◐` route still needs a browser pass before the feature is done.

### loop notes

- The [three blocker classes from background](#background) often get missed when fixing in place. Caching a downstream fetch (`getThing(id)`) doesn't clear an `await params` at the top of the page body — push the param promise into the `<Suspense>`-wrapped child.
- Ambiguous calls are user check-ins, not agent judgment. When you're not sure which fix fits, the blocking code looks security-sensitive, or the user might want to keep the route blocking on purpose — read [references/per-page-decisions.md](./references/per-page-decisions.md) before editing. Show the route while you ask: the `next-dev-loop` session runs the browser headed, so drive to the page and leave it on screen so the user is looking at the thing they're deciding about, with a screenshot as the fallback when a headed browser isn't possible. "Should this stay blocking?" is much easier to answer while looking at the page than at a file path.
- Don't narrate the refactor with comments. The only comment the codemod (or you) should leave is `// TODO: Cache Components adoption` on opt-outs, and the user's existing comments. Don't annotate every `<Suspense>` boundary or `"use cache"` call with what it does — the code says that. Drop a comment only when the _why_ isn't clear from the code (e.g. a deliberate Block with a reason).
- For many routes with the same mechanical fix, verify one representative route first. Then batch disjoint route groups using the same recipe, and run the shared build and browser checks together.

Keep a todo list of the feature's routes. When every route in the feature is clean, move to step 3.

## step 3: verify the feature

Checklist before checking in with the user:

- `next build` completes without blocking-route errors.
- No bare TODOs in the feature: `grep -rn "TODO: Cache Components adoption"` finds both the codemod's opt-out comments and the sync-IO unblocks from the pre-step. Any `instant = false` left behind is a deliberate, documented Block — comment rewritten to a reason (see [references/per-page-decisions.md](./references/per-page-decisions.md) → "when to leave a Block in place"). Any `await connection()` left behind has been reviewed and kept on purpose, not left over from the pre-step.
- Each route visited in the browser: confirm the static shell renders first and every `<Suspense>` fallback resolves to its real content. Capture both states if you can — the fallback (mid-stream) and the final paint — so you have a streaming-experience demo to show the user. Throttle the network in the browser if streaming is too fast to observe.
- If runtime verification fails, reproduce the same route on the pre-adoption branch or with its opt-out restored. A failure that already exists is an environment or data problem, not an adoption regression.

Then check in with the user. Same rule as the pre-step: speak their language. Don't say "feature-by-feature loop" or other internal labels; talk about the feature you adopted and what the user will see.

- What you did: which routes you touched, and the user-visible result per route (e.g. "the post page now streams the article body behind a skeleton while the layout stays static").
- What changed: opt-outs removed, fallbacks added, caching boundaries introduced.
- Show, don't tell. The `next-dev-loop` session runs the browser headed, so drive the route live for the user so they see the static shell → fallback → final content sequence in real time. If you can't drive a live browser, attach the before/after screenshots you captured instead.
- Give them the click-through: a short table of the feature's routes — the URL to open and what to look for (what renders instantly, which fallbacks appear, what streams in) — so they can verify each one themselves.
- The question: "Want to open this feature as a PR and move on to the next, or stop here?" Wait for the answer.

**Trivial features can skip the check-in.** If adopting a feature only meant removing its `// TODO: Cache Components adoption` opt-out (no `<Suspense>` added, no `'use cache'` introduced, no render order change), the user sees nothing different. Move on to the next feature without stopping; mention it in passing the next time you do check in.

When the loop has run on every feature — every remaining `instant = false` sits under a reason comment, `grep -rln "TODO: Cache Components adoption" app` returns nothing — point the user at [further reading](#further-reading) if they want to push the experience further, or stop and ship.

### route table glyphs

`ƒ` → `◐` is where adoption usually lands. `◐ (Partial Prerender)` means a static shell prerenders and the request-time content streams in — the goal state for any route that reads `cookies()`, `headers()`, `params`, or `searchParams`. Some routes legitimately stay `ƒ` when they do request-time work through a documented escape hatch (e.g. a layout that uses `await connection()`); the page is no longer _opted out_, it's genuinely dynamic. Don't remove the escape hatch only to chase a `◐`. The inverse holds: `instant = false` does not force a route to be `ƒ`. The glyph reflects what the route does at prerender time, not which validation knobs it exports.

`◐` tells you a shell exists, not what's in it. A `<Suspense>` boundary placed too high (e.g. wrapping the entire page body, or `<Suspense fallback={null}>` around the article content) pushes the visible content out of the static shell into the streamed payload; the build still reports `◐` because _some_ shell prerendered (often only `<html><body>` with framework markup). The route table can't tell you what's in the shell; a browser can. If the shell is empty and everything streams, pull the `<Suspense>` boundary down closer to the actual dynamic read.

## further reading

The work below is optional and lives in the docs — link the user to them and let them decide which to take on next. Don't walk these through inside this skill.

- [Sweep for more instant navigations](./references/dev-only-validations.md) — an optional follow-up once adoption is done, never required. A passing build is not the last word, because dev validates every route on each page load (simulating both page loads and client navigations) and catches what the build's first-error exit and descendant shadowing skipped. Offer it as the smaller path to instant navigation for a user who doesn't want to adopt Partial Prefetching. Adopting Partial Prefetching (below) runs the same kind of loop and meets these insights anyway, so recommend both and let the user pick which, or whether. The reference is the loop to execute.
- [`next-partial-prefetching-adoption`](https://github.com/vercel/next.js/tree/canary/skills/next-partial-prefetching-adoption) — the follow-up skill that adopts Partial Prefetching: it enables `partialPrefetching` and audits every `<Link prefetch={true}>` against a decision table (or adopts incrementally with the flag off, driven by the `instant-link-prefetch-partial` insight). It sequences this the same way this skill sequences Cache Components, but the insights are dev-only, so it's a browser click-through, not a build loop. Recommended after instant navigation, since those fixes feed directly into how much of each route the shell can prefetch. Concepts live in the [Adopting Partial Prefetching guide](https://nextjs.org/docs/app/guides/adopting-partial-prefetching).
- [Prevent regressions with e2e tests](https://nextjs.org/docs/app/guides/instant-navigation#prevent-regressions-with-e2e-tests) — the `@next/playwright` [`instant()`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/instant#testing-instant-navigation) helper asserts on the UI that's available immediately on navigation, so regressions surface in CI. Recommend it once a route is instant: `next-dev-loop` confirms it _now_; an `instant()` test keeps it that way.
- [`next-cache-components-optimizer`](https://github.com/vercel/next.js/tree/canary/skills/next-cache-components-optimizer) — a separate skill that grows each route's static shell so more of the page prerenders and less streams in. Pure optimization, not part of adoption.

<!-- chapter:end slug=next-cache-components-adoption -->

---

<!-- chapter:begin slug=next-cache-components-optimizer position=16 -->

## 16. next-cache-components-optimizer

- **Source:** https://github.com/vercel/next.js/blob/canary/skills/next-cache-components-optimizer/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/skills/next-cache-components-optimizer/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/next-cache-components-optimizer.md
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

Bundled files (5), referenced from this skill's directory:
  - `reference/patterns.md` — https://raw.githubusercontent.com/vercel/next.js/canary/skills/next-cache-components-optimizer/reference/patterns.md
  - `reference/real-app-patterns.md` — https://raw.githubusercontent.com/vercel/next.js/canary/skills/next-cache-components-optimizer/reference/real-app-patterns.md
  - `reference/red-test-robustness.md` — https://raw.githubusercontent.com/vercel/next.js/canary/skills/next-cache-components-optimizer/reference/red-test-robustness.md
  - `rig-template.md` — https://raw.githubusercontent.com/vercel/next.js/canary/skills/next-cache-components-optimizer/rig-template.md
  - `test-template.md` — https://raw.githubusercontent.com/vercel/next.js/canary/skills/next-cache-components-optimizer/test-template.md

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
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

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.

## 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()`](https://nextjs.org/docs/app/guides/instant-navigation#prevent-regressions-with-e2e-tests)
  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.

## 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`).

## 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.

## 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`.

## 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. PREREQUISITES: current Next.js with Cache Components

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`](https://github.com/vercel/next.js/tree/canary/skills/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.

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

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.

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

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).

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

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.**

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

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.

---

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

**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.

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

```
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.

### 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.

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

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.

See: [Streaming](https://nextjs.org/docs/app/guides/streaming#push-dynamic-access-down)
and [loading states](https://nextjs.org/docs/app/guides/instant-navigation#iterate-on-loading-states).

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.

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

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](https://nextjs.org/docs/app/guides/optimizing-prefetching)
and pattern 10 in `reference/patterns.md` for the requirements, cost trade-offs,
manual prefetch caveat, and `instant()` test gotchas.

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

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.

## F. DIFFERENTIAL

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.

## G. REVIEW (PR checklist)

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.

## 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`).

## 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.

## 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`](https://github.com/vercel/next.js/tree/canary/skills/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.

<!-- chapter:end slug=next-cache-components-optimizer -->

---

<!-- chapter:begin slug=next-dev-loop position=17 -->

## 17. next-dev-loop

- **Source:** https://github.com/vercel/next.js/blob/canary/skills/next-dev-loop/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/skills/next-dev-loop/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/next-dev-loop.md
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: next-dev-loop
description: >
  Verify Next.js runtime behavior after editing app code. Use this
  skill to confirm a change actually works in a running app — not
  just that it compiles or type-checks. Combines /_next/mcp
  (Next.js's view) with agent-browser (the browser's view).
  Requires a running `next dev`.
---

# next-dev-loop

The edit/verify rhythm during `next dev` — make a change, then
confirm it actually works at runtime, not only that the types or
the build are happy.

You verify through two views of the same running app:

- **`/_next/mcp`** — an HTTP endpoint Next.js exposes about itself.
  Knows framework-specific things: routes, segments, RSC, server
  actions, server logs, and errors as Next.js saw them. Call
  `tools/list` for the current surface.
- **`agent-browser`** — a CLI that drives a real Chrome. Knows
  framework-agnostic browser things: DOM, console, network, React
  fiber, vitals. Before driving it, run `agent-browser skills get core`
  once for the version-matched usage guide — don't guess subcommands
  from memory.

The two views cross-check each other.

## requires

- Next.js **16.3+** with **Turbopack** — `/_next/mcp` plus the
  proactive compile check via `get_compilation_issues`.
- `agent-browser` **>= 0.31.1** — React introspection, worktree-scoped
  `session id`, idempotent `--restore`, and launch flag reconciliation.

These are hard floors, not soft preferences. If anything is missing,
tell the user how to upgrade and stop. Don't fall back to grepping
source or to a weaker probe — this skill assumes both views are live
at the versions above.

- Upgrade Next.js: `pnpm next upgrade` (or `npx next upgrade`).
  Docs: https://nextjs.org/docs/app/getting-started/upgrading
  (version-16 guide:
  https://nextjs.org/docs/app/guides/upgrading/version-16)
- Install or upgrade `agent-browser`: `npm i -g agent-browser@latest`.
  If the CLI isn't on `PATH`, install it before continuing — preflight
  expects to invoke it directly.

## preflight

Once per session, confirm both views are live.

1. **Open `agent-browser` at the target URL, restoring saved
   login state when present.** First derive one stable session id for
   this checkout and use it for every `agent-browser` command:

   ```bash
   SESSION="$(agent-browser session id --scope worktree --prefix next-dev-loop)"
   export AGENT_BROWSER_SESSION="$SESSION"
   export AGENT_BROWSER_RESTORE="$SESSION"
   ```

   Then open the target URL:

   ```bash
   agent-browser --session "$SESSION" --restore --headed --enable react-devtools open <url>
   ```

   `--scope worktree` keeps parallel worktrees and copied checkouts
   from colliding. Bare `--restore` uses the session id as the
   persistence key, loads saved cookies/localStorage before navigation
   when present, and auto-saves state on close. Always pass the desired
   launch flags on `open`; agent-browser will reuse, relaunch, or restart
   its scoped background state as needed.

   The browser is the user's. If state was not restored (first run,
   expired session) and the page is gated, the user drives the login —
   pause until they confirm. After login, continue using the same session
   and restore context; `agent-browser close` saves the cookie state so
   the next `open` restores it.

2. Probe `/_next/mcp` (`tools/list`) — confirm it's reachable and
   lists `get_compilation_issues`. First read the port off the
   `next dev` banner; if it isn't 3000, set
   `NEXT_MCP_URL=http://localhost:<port>/_next/mcp` before probing:
   - Unreachable → either `next dev` isn't running, or Next.js is
     below 16.3. Check `package.json` to disambiguate, then refuse.
   - `get_compilation_issues` not in the list → Next.js below 16.3.
     Refuse and tell the user to upgrade.
3. `get_compilation_issues` doubles as a Turbopack probe. An error
   response of `"Turbopack project is not available..."` means the
   user is on webpack. Refuse — Turbopack is required.
4. `get_routes` → your route map for the rest of the session.

## loop

### before the edit — narrow the scope

Ask the running app, not the codebase. `/_next/mcp` knows which
files rendered the current route; use those as your search scope.
Runtime introspection stays cheap as the codebase grows; agentic
search doesn't.

### after the edit — verify

Four failure modes. Check each:

- **Compiles** — `get_compilation_issues`.
- **Runs without errors** — `/_next/mcp` (server and bubbled-up
  browser errors both surface here).
- **Behaves as intended** — `agent-browser` drives the page; assert
  what the user actually sees.
- **React-level behavior** — `agent-browser` with react-devtools
  enabled exposes the component tree, props, state, and render
  counts. Anchor framework-level checks here (extra renders,
  server/client boundary shifts, suspense fallbacks) — DOM asserts
  alone miss them.

Pick the specific tool from `tools/list` or the agent-browser
manual rather than from memory.

## gotchas

- **Every `agent-browser` command must know your session and restore
  key, or it may use an empty default browser or fail to save login
  state.** Easiest: export both `AGENT_BROWSER_SESSION="$SESSION"` and
  `AGENT_BROWSER_RESTORE="$SESSION"` at the top of each shell you run
  agent-browser in. If you do not export them, pass
  `--session "$SESSION" --restore` on every command.
- **When the two views disagree, suspect the tooling first.** If
  `agent-browser` says a route is broken but `/_next/mcp` and the
  server say it rendered cleanly, a stale or misdirected browser
  session is the likelier cause than a real bug — reconcile the views
  before debugging the app.
- Confirming a click or navigation: the page settles a beat later, so
  wait with `wait --load networkidle` (no path to get wrong), then
  snapshot/read to confirm the page. Avoid `wait --url` unless you pass
  the link's exact href — a guessed or placeholder path won't match the
  real URL and times out after 25s.
- A blank read, empty snapshot, `about:blank`, or a "no browser
  session" error — right after `open` or after a click (even if `open`
  reported the page) — is the browser dropping the page (a stale
  session), not a broken route. Reopen your session at the URL with
  `--session "$SESSION" --restore` and re-snapshot; if still blank,
  run `agent-browser --session "$SESSION" --restore close`, then open
  again. Don't fall back to `curl`; it bypasses the browser you're
  testing.
- React introspection output is stale after navigation. Re-run.
- `/_next/mcp` replies are SSE — read the JSON off the `data:` line
  with `sed -n 's/^data: //p'` (a plain `sed 's/^data: //'` leaves the
  `event:` line and the parse fails).
- `get_errors` and `get_page_metadata` need at least one navigation
  to populate.

## reference

All tools below are present once preflight passes. If `tools/list`
is missing any of them, preflight should have refused — re-check.

```
# /_next/mcp                 notes
get_project_metadata         projectPath, devServerUrl, bundler
get_routes                   fs-scan; no browser session needed
get_errors                   runtime + build; needs a browser session;
                             includes browser-side errors caught by the
                             dev server
get_page_metadata            segment trie + routerType; needs a browser
                             session; use as a discovery shortcut for
                             which files power a route
get_logs                     returns logFilePath
get_server_action_by_id      hashed id → file + functionName
get_compilation_issues       Turbopack only; errors on webpack
                             ("Turbopack project is not available")
```

## teardown

Close the session with the same session and restore context:
`agent-browser --session "$SESSION" --restore close`. `close` saves
that session's cookies and storage so the next loop's `--restore` open
keeps the user logged in. Leave `next dev` up for the next loop.

<!-- chapter:end slug=next-dev-loop -->

---

<!-- chapter:begin slug=next-partial-prefetching-adoption position=18 -->

## 18. next-partial-prefetching-adoption

- **Source:** https://github.com/vercel/next.js/blob/canary/skills/next-partial-prefetching-adoption/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/skills/next-partial-prefetching-adoption/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/next-partial-prefetching-adoption.md
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

Bundled files (1), referenced from this skill's directory:
  - `rig-template.md` — https://raw.githubusercontent.com/vercel/next.js/canary/skills/next-partial-prefetching-adoption/rig-template.md

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: next-partial-prefetching-adoption
description: >
  Turn on Partial Prefetching in a Next.js app and work through the
  insights it surfaces. Use when the user wants to enable or adopt
  Partial Prefetching, flip the `partialPrefetching` flag, opt routes
  in with `export const prefetch = 'partial'`, audit
  `Link prefetch={true}` behavior, preserve existing prefetched UI
  with `instant()` tests, or resolve the
  instant-link-prefetch-partial and instant-shell-url-data insights.
---

# next-partial-prefetching-adoption

Enable Partial Prefetching and walk the app until every link reuses a shared App Shell. This skill sequences the work; per-insight recipes live in the dev overlay fix cards and their docs pages. The [Adopting Partial Prefetching guide](https://nextjs.org/docs/app/guides/adopting-partial-prefetching) is the canonical reference for the concepts this skill applies.

The development insights and the preservation tests are two different paths. Insights surface only in `next dev`, in the dev overlay's Insights tab. Test-backed preservation runs against a production-like build with `instant()` and does not need a development server. After the flag is enabled, the separate URL-data insight sweep still uses `next dev`.

## preservation gate

When using test-backed preservation, the first implementation milestone is a passing flag-off `instant()` suite. Set up the production test rig, write the selected assertions, run them with `partialPrefetching` disabled, and record the command and exit status. Test-only configuration required by the rig is allowed, but until that baseline passes, do not enable `partialPrefetching` or edit the destination, cache boundaries, or Link props. Installing missing test dependencies is part of reaching the baseline, not a reason to adopt first. Use the manual path only when `rig-template.md` identifies a concrete blocker the repository cannot resolve, and record the blocker and deferred test coverage.

Talk to the user in terms of what they'll see — PRs, features, and how the app behaves after — never the insight slugs or step labels. Before you start, tell them briefly what Partial Prefetching changes: links to a route prefetch one shared App Shell, and `prefetch={true}` can also resolve cached URL-specific content. The audit determines which UI from the legacy full prefetch to preserve.

## requires

- **Cache Components adopted (`cacheComponents: true`) with a passing build.** Both `partialPrefetching` and the route-level `prefetch` export require Cache Components. If it is off, use [`next-cache-components-adoption`](https://github.com/vercel/next.js/tree/canary/skills/next-cache-components-adoption) first and return after its build-blocking prerender errors are resolved. Those errors can fail `next build`; only the Partial Prefetching insights handled by this skill are non-blocking development signals.

- **Next.js 16.3 or later.** `partialPrefetching`, the `prefetch` route segment config, and the prefetch insights all land there.

- **A browser you can drive.** Test-backed preservation uses an existing or minimal production-mode Playwright suite; manual preservation and the final demonstration use the running production app. The development insight path and the post-flag URL-data sweep use [`next-dev-loop`](https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop); install it before either development pass unless it is already available (`npx skills add https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop`). If the app is webpack-pinned, drive a browser directly (`agent-browser`, Playwright) — you lose the framework cross-checks, not the insights; they're still in the overlay and the dev log.

- **A runnable app.** Preservation and the final demonstration need a production-like build because automatic prefetching runs only in production. The development server is required only when using the insight path or running the post-flag URL-data sweep; do not start it merely to confirm a test-backed preservation case. If the app reads a database or required environment at import, confirm the environment used by the chosen path can start before step 1.

### notes

- **Offline docs.** Guide links have offline copies under `node_modules/next/dist/docs/` (bundled since Next.js 16.2), with the directory layout numbered for ordering (e.g. `node_modules/next/dist/docs/01-app/02-guides/adopting-partial-prefetching.md`). If you can't predict the numbered prefix, `find node_modules/next/dist/docs -name '<slug>.md'` resolves it. The `/docs/messages/*` error pages are not bundled.

- **Older versions without bundled docs.** Suggest `npx @next/codemod@latest agents-md` to the user before starting: it downloads a version-matched copy to `.next-docs/` and writes an index into `AGENTS.md` / `CLAUDE.md`. It touches files in their repo, so ask first and run it only if they want it.

## background

Adopting Partial Prefetching means every route preserves the prefetched UI that matters, now split between the shared App Shell and any extra per-link data a link explicitly asks for. The [guide](https://nextjs.org/docs/app/guides/adopting-partial-prefetching) is the canonical reference for what a prefetch contains and how to decide each case; this skill sequences that work against a running app.

The catch that decides most of the sweep: a default link warms only the shared App Shell. A route keyed by `params` or `searchParams` can prefetch more only after it has adopted Partial Prefetching and a specific link uses [`<Link prefetch={true}>`](https://nextjs.org/docs/app/api-reference/components/link#prefetch); then Next.js resolves the URL data and any cached content behind it before the click (the guide's [URL data](https://nextjs.org/docs/app/guides/adopting-partial-prefetching#url-data) section).

## working surfaces

- **The production-mode `instant()` suite — the primary record for test-backed preservation.** Reuse the app's production build, test context, and Playwright setup. Read an existing `instant-nav.rig.md` first; if the project has no rig, create it from **`rig-template.md`**. The same tests define the legacy target before adoption and become the work queue after each destination opts into Partial Prefetching. Development can help investigate a failure, but only this suite decides whether the prefetched UI was preserved.
- **The dev server terminal — the primary record for the insight path.** Each validated route's insights are logged as `Error: Route "...": Next.js encountered ...` lines with the `https://nextjs.org/docs/messages/<slug>` link. Tail the dev log during the sweep; it's the greppable record of what fired where, and it works the same on Turbopack and webpack.
- **The dev overlay Insights tab.** Insights are the amber, non-blocking tab. It appears only once an insight has fired, so a route that surfaces nothing shows no tab at all — that's the clean state, not a missing feature. Don't hunt for the tab on a quiet route; confirm clean from the dev log above, which is the reliable signal. The precondition is no blocking-prerender errors — those replace the insight on their route (see requires). An unrelated Issue (a hydration error, a console error) doesn't block the sweep; don't stall on it. When the tab is present, the overlay pill shows the count and each insight has fix cards linking its docs page. The overlay renders inside a shadow root (`nextjs-portal`), so accessibility-tree snapshots don't see it — evaluate into `shadowRoot` when you need to read or click it programmatically.
- **`next-dev-loop`** to drive navigations and read the overlay. Prefer it over hand-rolled browser automation for the same reasons as in the Cache Components skill (webpack apps: see requires). When browsing its `/_next/mcp` tools, the prefetch insights surface through `get_errors` and the overlay, not the similarly-named `get_request_insights`. That one is the span and performance recorder (gated behind `experimental.requestInsights`) and reports nothing about prefetching.

Every insight has a docs page — open it. Fetch the linked page for every distinct insight you encounter; the inline message is a summary, the page is the recipe.

## step 1: audit `<Link prefetch={true}>` navigations (before enabling)

Keep the global flag **off** through this audit and the legacy baseline in step 2. Enabling it earlier would remove the legacy behavior the migration needs to measure. If the flag is already on in unshipped work, use the pre-flag commit for the audit and baseline. When the user is available, ask how to ship it in the language of PRs:

- **One branch** — the whole audit in one change, with the flag enabled and the codemod run at the end (step 4).
- **Route by route** — each adopted destination ships as its own PR. The insight still fires for the destinations you haven't reached, a live worklist, and step 4 comes after the last one.

The work and its order are identical either way — only the commit boundaries differ. When no user is available, default by app size: one branch for a handful of links, route by route when the audit is big enough that reviewers need smaller diffs. Note the choice in your report.

Enumerate explicit prefetch and manual prefetch sites across the whole source tree, not only `app/` — they often live in `src/components` or shared UI packages. Start from `next/link` imports and re-exports, then follow custom wrappers to their consumers. Use `rg -n '\bprefetch\b|router\.prefetch' -g '*.tsx' -g '*.jsx' .` as a candidate list, not as the complete audit; inspect conditional props and forwarded `LinkProps` to determine the effective production value. Include every audited navigation whose effective production Link value is `prefetch={true}`: explicit `true`, a bare `prefetch` prop, and expressions that resolve to `true`. Exclude the default value, `prefetch="auto"`, and `prefetch={false}` from the preservation suite because they do not request the legacy full prefetch. Audit existing [`router.prefetch()`](https://nextjs.org/docs/app/api-reference/functions/use-router#userouter) calls separately because they have no Link insight. For new manual prefetching, follow the [Prefetching guide](https://nextjs.org/docs/app/guides/prefetching#manual-prefetch). If no Link resolves to `prefetch={true}`, say so and move on to [step 4](#step-4-enable-the-flag).

### Choose what to preserve and how to verify it

Before writing tests or editing destinations, follow the guide's [migration guidance](https://nextjs.org/docs/app/guides/adopting-partial-prefetching#migrate-existing-full-prefetches) to propose the UI worth preserving. Present the result in one concise table:

| Navigation | Proposed result |
| ---------- | --------------- |

Group equivalent navigations. Summarize what will be ready immediately and what will stream. When a proposal is ambiguous, show the navigation in the running app and ask the user to confirm it. If they are unavailable, follow the guide and record the assumption.

After the target UI is settled, inspect the existing test setup. The `instant()` helper comes from the separate [`@next/playwright`](https://nextjs.org/docs/app/guides/instant-navigation#prevent-regressions-with-e2e-tests) package, not `next/experimental/testmode/playwright`.

- **Applicable production-mode suite:** use test-backed preservation by default. Reuse the project's `@next/playwright` tests, production scripts, authentication, and existing `instant-nav.rig.md`. Follow the guide's [prefetched UI test workflow](https://nextjs.org/docs/app/guides/adopting-partial-prefetching#verify-prefetched-ui-with-tests) and make the complete flag-off suite green before adoption. The unchanged assertions drive the migration and stay as regression coverage.
- **No applicable production-mode suite:** set up the production-mode rig in **`rig-template.md`** using the project's package manager and test conventions. This is part of test-backed adoption and does not require a user to be present.
- **Rig cannot run reliably:** work through **`rig-template.md`** setup and liveness checks. Fall back to manual preservation only for a concrete blocker the repository cannot resolve, such as unavailable credentials or an inaccessible production environment. Record the blocker and the deferred test coverage; do not claim test-backed verification.

No user input is required to reuse an existing suite or create the rig. Ask only when the repository cannot answer an environment question or when the target UI itself is a product decision. If no user is available, use the guide's safe product default and reserve manual verification for a concrete rig blocker. Treat new prefetched UI as step 7 work; verify any deliberate removal separately after adoption.

This workflow is specific to a clicked `<Link>`. A direct call such as `router.prefetch('/dashboard')` is a manual prefetch, not a Link prefetch; keep it in the source audit and verify it separately in step 6.

## step 2: capture the legacy baseline

Do not enable `partialPrefetching` or edit route behavior, Link props, or cache boundaries during this step. Test-only configuration required to run `instant()` is allowed.

For test-backed preservation, complete the [preservation gate](#preservation-gate): write the complete `instant()` suite and **run it** against the production-like rig with Partial Prefetching disabled. A test file, build, completed navigation, or command printed for the user is not a baseline. Do not continue to step 3 until the suite has actually passed.

For manual preservation, finish the before/target inventory before editing any destination. Fall back to this path only for a concrete rig blocker identified through `rig-template.md`, and record the blocker and deferred tests.

## step 3: adopt destinations and restore the target

Adopt every audited destination with the temporary route config. The route export is enough for the unchanged tests to exercise Partial Prefetching on that destination while the global flag remains off:

```tsx
// See: https://nextjs.org/docs/app/guides/adopting-partial-prefetching
export const prefetch = 'partial'
```

If other URL-specific UI might be worth prefetching but was not part of the legacy contract, keep `prefetch={true}` on its links and mark the route for step 7:

```tsx
// TODO(per-link-prefetch): assess with the user whether URL data should resolve before click.
// See: https://nextjs.org/docs/app/guides/optimizing-prefetching
export const prefetch = 'partial'
```

Use that exact prefix so step 7 can grep them back. Do not select new target UI now; restore only the target chosen from the legacy behavior.

For test-backed preservation, rerun the affected **unchanged** tests after each destination changes and treat failures as the work queue. Run the complete suite and record its passing exit status before enabling the global flag. For manual preservation, compare the adopted production navigation with the selected target and document anything not yet restored. Apply the guide's matching preservation pattern for caching and Link-prop changes, and ask the user before making an unclear freshness or caching decision. New URL-data candidates marked above wait for step 7.

When restoring the target changes caching or invalidation, follow the project's existing verification approach. Reuse or extend an applicable suite for the affected lifecycle, such as freshness after mutations, cache scope, or generated values. If the project doesn't test this type of behavior, do not introduce new test infrastructure during adoption; verify it manually in production and record the expected and observed results. A green `instant()` test proves readiness, not cache correctness. Ask the user only when the intended behavior is unclear.

> **If you add `use cache`, verify under `next start`, not only the build.** A `cookies()`/`headers()`/session read anywhere in the cached call tree throws at request time while `next build` passes clean. See [`use cache`](https://nextjs.org/docs/app/api-reference/directives/use-cache).

## step 4: enable the flag

Once every audited destination has `prefetch = 'partial'`, finish in two moves.

1. **Enable the flag globally.** Set `partialPrefetching: true` in `next.config.ts` (alongside `cacheComponents: true`). Every route is adopted now, so every link is good.
2. **Strip the redundant `prefetch = 'partial'` exports.** Run the first-party `remove-partial-prefetch` codemod rather than a text find-and-replace. It removes every `export const prefetch = 'partial'`, including exports below a `TODO(per-link-prefetch)` marker, and removes its generated Partial Prefetching guide comment. The TODO marker and its Optimizing prefetching guide link stay for step 7. Other values such as `prefetch = 'force-disabled'` stay in place.

   ```bash
   npx @next/codemod@canary remove-partial-prefetch ./app
   ```

   Use `./src/app` in a `src/` project and check the reported file count. The codemod refuses to run on a dirty working tree. Commit or stash unrelated work first, or pass `--force` to let its edits land alongside your WIP. If the codemod isn't available (older `@next/codemod`, sandboxed environment, offline run), reproduce it by hand by removing `export const prefetch = 'partial'` and its generated Partial Prefetching guide comment from every `app/**/{page,layout}.{js,jsx,ts,tsx}` — leave other `prefetch` values in place, and leave the `TODO(per-link-prefetch)` markers and Optimizing prefetching guide links where they are. Don't hand-edit when the codemod can run.

After the flag and codemod land together, rerun the locked preservation suite when using the test-backed path. Otherwise repeat the documented production comparisons under the final global configuration.

## step 5: sweep for URL-data insights (after enabling)

This is a dev-only second pass. The shell check runs only with the flag on, fires at navigation time, and never blocks the build, so it can happen any time after step 4. Build the route queue from a concrete source (the last `next build` route table, or the `app/` tree) and keep it as a todo list.

Sweep feature by feature. A feature is a single product surface — `app/settings/**`, `app/posts/[slug]/**` — not a whole top-level area. Finish one end-to-end before starting the next: load its routes in `next dev` and resolve their insights. The insight never blocks the build and each route is independent, so a partial sweep leaves a working app, and each feature is a self-contained change the user can review or ship on its own.

If the environment can't finish the whole sweep (slow first compiles, a dev server that falls over under load, no browser at all), take the browser-free work as far as it goes before handing off. Adopt every route you can statically: apply the fix from [`URL data`](https://nextjs.org/docs/messages/instant-shell-url-data) up to a new `<Suspense>` boundary, gating on type-check. Work the whole queue in one pass — a larger refactor isn't a reason to defer, and asking whether to continue to the next route or tier isn't a checkpoint; keep going. Stop only for a genuine judgment call, and batch those into the single hand-off report: the routes you statically adopted, the ones still needing a live shell check, and the queue.

Watch the Insights tab and the dev log for `Next.js encountered … data` lines. The signal this step adds is [`URL data`](https://nextjs.org/docs/messages/instant-shell-url-data): a `params` or `searchParams` read too high in the suspended subtree ties the shared shell to one URL. This insight is narrow; it most reliably appears on a `generateStaticParams` route where `params` is already under `<Suspense>`, but still awaited before the URL-specific leaf boundary. If a `blocking-prerender-*` error fires instead, apply the same structural fix.

Loading a route with the flag on prerenders its App Shell, which validates more of the route than the Cache Components build did. So a route that built cleanly under Cache Components (every route `◐`, no errors) can still surface a `blocking-prerender-*` error here the first time its shell is prerendered — [`runtime data`](https://nextjs.org/docs/messages/blocking-prerender-runtime) (`cookies()`/`headers()`), [`uncached data`](https://nextjs.org/docs/messages/blocking-prerender-dynamic) (an uncached `fetch`/DB call), or sync IO like `Date.now()`/`new Date()`. This doesn't mean the Cache Components adoption was incomplete; it's new validation reaching a path the build never exercised. These aren't Partial Prefetching insights — fix each one the same way you would any blocking-prerender error.

These fixes rarely involve the user — each insight names the offending read and its docs page has the fix, so apply it and keep sweeping. Collect the rare exceptions for one batched question at the end: a page that is entirely one URL-dependent region (wrapping it all leaves an empty shell), or a route that should arguably stay opted out. Don't narrate the refactor with comments — the `<Suspense>` boundaries speak for themselves.

## step 6: verify

Checklist before checking in with the user:

- **An empty sweep is expected when Cache Components adoption finished cleanly.** A quiet log is success, not a missing signal. If you deliberately probe the validation path, use a `generateStaticParams` route with `params` read inside `<Suspense>` but before the URL-specific leaf boundary; other shapes may surface `blocking-prerender-*` instead.
- The App Shells are real: for each route you changed, confirm the first paint after a navigation shows the intended shared content, not an empty shell or a stuck fallback. A `<Suspense>` around the whole page body passes validation with an empty shell, which defeats the point.
- The insights validate shell _structure_, not that a prefetch actually happened. Confirm on the production run (automatic prefetching runs only in production) that navigating a changed link lands on the shared shell instantly.
- For test-backed preservation, every locked `instant()` test for an audited `<Link prefetch={true}>` passes against the production run. For manual preservation, the before/after inventory and any deferred test follow-ups are recorded.
- Any caching or invalidation changed to preserve the target is verified through an applicable existing test suite or a recorded manual check when the project has no such coverage.
- **If the app prefetches imperatively**, the insight sweep does not cover it, so an empty sweep is not proof the prefetch survived the flag. Verify the call under `next start`: compare the `_rsc` prefetch response or resource timing before/after, and make sure any intentionally preserved full prefetch still carries the data the old call was warming. If it now returns only the App Shell, migrate that call site using the same decision as the nearest `<Link prefetch={true}>` destination — cache the data, or move per-link-prefetch behavior to a docs-supported `<Link prefetch={true}>`.
- **Before blaming a broken route on the flag, reproduce it with `partialPrefetching` off** (or on the pre-flag branch). The flag surfaces existing issues — a fragile request-time auth gate, a rewrite, deployment skew — earlier and more visibly, but rarely causes them. If it breaks flag-off too, it isn't a Partial Prefetching problem; fix it there, not here.
- `next build` still passes.

Then check in with the user. Speak their language — no insight slugs or step labels.

- What you did: which links you audited, which destinations you adopted, and what each link now prefetches.
- What changed: dropped props, `use cache` boundaries added, and which routes carry a `TODO(per-link-prefetch)` marker for later.
- Demo against a production run. Automatic prefetching runs only in production, so `next dev` won't show the result — run `next build` and `next start`, and hand the user that URL. That run needs the app's real environment (database, auth, secrets), and a partial or stale install or leftover generated artifacts can fail the build for reasons unrelated to the adoption. Set the expectation up front that verification is a complete, credentialed production run, not a quick check.
- Show, don't tell: drive one link live in the headed browser against the production server, so they see the shared App Shell paint instantly and the URL-specific region stream in. Attach before/after screenshots only when a live browser isn't possible.
- Give them the click-through: a table of each changed route — the link to click, and what to expect after the click (what paints instantly, what streams in) — so they can verify each result themselves.
- The question: "Want to commit this (or open the PR) before we look at which routes should also prefetch their URL-specific content?" Wait for the answer — adoption and per-link prefetching read best as their own changes.

## step 7: per-link prefetching (optional)

The audit marked candidates beyond the already-preserved legacy contract instead of deciding them. Grep for `TODO(per-link-prefetch)` and walk the list with the user in one conversation. The question per route is whether they want the additional URL-dependent content prefetched ahead of the click, or streaming in after navigation is fine. A per-link prefetch costs a server invocation per prefetchable link — the guide's [trade-offs](https://nextjs.org/docs/app/guides/optimizing-prefetching#trade-offs) section is the checklist. Don't make these calls alone.

Where the answer is no, delete the marker and leave the route on the App Shell default. Where the answer is yes, follow the [Optimizing prefetching guide](https://nextjs.org/docs/app/guides/optimizing-prefetching), confirm the opted-in link against a production run, and delete the marker when the selected result is verified.

No `TODO(per-link-prefetch)` marker survives the finished step. Per-link optimization remains a separate commit or PR from adoption.

Finally, show any effective `prefetch={false}` links in a concise `Navigation | Why it may no longer be needed` table. Explain that `false` disables all prefetching, while Partial Prefetching's default `auto` behavior prefetches only the shared App Shell, so opt-outs added to avoid legacy full-route prefetching may now be unnecessary. Invite the user to revisit them separately.

## further reading

- [Instant navigation](https://nextjs.org/docs/app/guides/instant-navigation) — the broader validation model and loading-state tooling.
- [Prevent regressions with e2e tests](https://nextjs.org/docs/app/guides/instant-navigation#prevent-regressions-with-e2e-tests) — use the `@next/playwright` `instant()` helper to build the flag-off baseline suite, then keep it as the CI regression guard.
- [`next-cache-components-optimizer`](https://github.com/vercel/next.js/tree/canary/skills/next-cache-components-optimizer) — grows each route's static shell so the App Shell carries more.

<!-- chapter:end slug=next-partial-prefetching-adoption -->

---

## Part: Credited skills

---

<!-- chapter:begin slug=deploy-release-test position=19 -->

## 19. deploy-release-test

- **Source:** https://github.com/vercel/next.js/blob/canary/.agents/skills/deploy-release-test/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/deploy-release-test/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/deploy-release-test.md
- **Origin:** Credited — installed into this repository, not published from it.
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

Bundled files (1), referenced from this skill's directory:
  - `agents/openai.yaml` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/deploy-release-test/agents/openai.yaml

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: deploy-release-test
description: >
  Validate a commit-specific Next.js preview package and manually trigger the
  entire Next.js deployment test suite through the test_e2e_deploy_release.yml
  GitHub Actions workflow. Use only when asked to run the full deploy test suite
  or this workflow specifically from an internal vercel/next.js PR branch. Do
  not use for focused deployment-test sanity checks; run the relevant tests
  locally with pnpm test-deploy instead. Covers resolving the latest branch SHA,
  waiting for vercel-packages, preserving default workflow inputs, dispatching
  the workflow, and verifying the run.
metadata:
  internal: true
---

# Deploy Release Test

Use this skill only when manually running the entire Next.js deployment test
suite for a pull request. Treat package validation as a hard gate: never
dispatch the workflow until the exact commit's redirected tarball responds
successfully.

## Scope

Do not use this workflow to sanity-check one deployment test or a focused group
of tests. Follow
[Running Deploy Tests Locally](../../../contributing/core/testing.md#running-deploy-tests-locally)
instead:

```bash
NEXT_TEST_VERSION=https://vercel-packages.vercel.app/next/commits/<commit-sha>/next pnpm test-deploy <path-to-test>
```

If a request to "run deploy tests" does not explicitly call for the entire
suite, prefer the focused local workflow and scope it to the affected tests.

## Inputs

- Accept a PR number or determine the PR from the current branch.
- Use repository `vercel/next.js` and workflow
  `.github/workflows/test_e2e_deploy_release.yml`.
- Require the PR head branch to exist in `vercel/next.js`. A fork branch cannot
  run this secret-bearing workflow; use the repository's PR adoption process
  first when appropriate.

## Workflow

1. Resolve the PR branch and its latest commit from GitHub, not merely from the
   local checkout:

   ```bash
   gh pr view <pr-number> --repo vercel/next.js \
     --json number,url,headRefName,headRefOid,isCrossRepository
   ```

   If no PR number was supplied, omit `<pr-number>` to detect the PR from the
   current branch. Stop if `isCrossRepository` is `true`. Record `headRefName`
   as the branch and `headRefOid` as the commit SHA.

2. Construct the exact package URL:

   ```text
   https://vercel-packages.vercel.app/next/commits/<commit-sha>/next
   ```

3. Wait for the package to become downloadable before dispatching anything:

   ```bash
   node scripts/wait-for-preview-tarball.mjs --commit-sha <commit-sha>
   ```

   Keep the wait in an ongoing terminal session and poll it so the user still
   receives progress updates. The helper uses `HEAD`, follows the redirect to
   Vercel Blob, and only succeeds when the final artifact is available. Do not
   substitute a check that accepts the initial redirect: that endpoint can
   redirect even while the blob still returns 404.

   If the helper times out or reports an authorization or build failure, do not
   dispatch the workflow. Report the failure and inspect the commit's
   `build-and-deploy` / `upload-preview-tarballs` checks if useful.

4. Resolve the PR again immediately after validation. Compare the current
   `headRefName` and `headRefOid` with the recorded values. If either changed,
   return to step 2 and validate the new commit-specific URL. Never reuse the
   old package URL for a moved branch.

5. Trigger the workflow from the PR branch and pass only `nextVersion`:

   ```bash
   gh workflow run test_e2e_deploy_release.yml \
     --repo vercel/next.js \
     --ref <pr-branch> \
     -f nextVersion=https://vercel-packages.vercel.app/next/commits/<commit-sha>/next
   ```

   Do not pass any other `-f` values. Leaving them unspecified preserves the
   workflow defaults, including `vercelCliVersion: vercel@latest` and empty
   optional overrides.

6. Find and verify the newly created run:

   ```bash
   gh run list --repo vercel/next.js \
     --workflow test_e2e_deploy_release.yml \
     --branch <pr-branch> \
     --event workflow_dispatch \
     --limit 5 \
     --json databaseId,url,status,conclusion,headBranch,headSha,displayTitle,createdAt
   ```

   Confirm that the newest matching run has:
   - `headBranch` equal to the PR branch
   - `headSha` equal to the validated commit SHA
   - `displayTitle` containing the exact commit-specific package URL

   A race can still move the branch between the final check and dispatch. If the
   run's `headSha` differs, report the mismatch and do not trigger another run
   until the new SHA's package has been validated.

## Completion Report

Report the PR branch, validated SHA and package URL, workflow run URL, and its
current status. Do not wait for the full deployment suite unless the user asks
you to monitor it.

## Related Skills

- `$pr-status-triage` - Inspect failures if the deployment workflow does not pass.
- `$create-pr` - Create or update the internal PR branch before deployment testing.

<!-- chapter:end slug=deploy-release-test -->

---

<!-- chapter:begin slug=gate-tests position=20 -->

## 20. gate-tests

- **Source:** https://github.com/vercel/next.js/blob/canary/.agents/skills/gate-tests/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/gate-tests/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/gate-tests.md
- **Origin:** Credited — installed into this repository, not published from it.
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: gate-tests
description: >
  How to use the `@gate` / `@force-gate` test directives instead of `it.skip`
  or fake-green skip patterns. Use when a test is known-failing under some
  test-matrix dimension (dev mode, a bundler, an experimental flag like
  cacheComponents), when converting `if (isNextDev) return` guards or
  env-var `describe.skip` branches, when adding a condition to
  test/lib/gate/conditions.ts, or when keying a fixture's experimental flag
  on a __NEXT_TEST_AXIS letter. Covers directive choice, condition tiers,
  the test-axis fixture pattern, pitfalls, and verification commands.
user-invocable: false
metadata:
  internal: true
---

# Gating tests with `@gate` / `@force-gate`

Full reference: [`test/lib/gate/README.md`](../../../test/lib/gate/README.md).
This skill is the decision guide: which directive to reach for, the standard
conversion patterns, and how to verify.

## Never write these — gate instead

| Anti-pattern                                                           | Replacement                                                                 |
| ---------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `it.skip('...')` for a known failure                                   | `// @gate <cond>` (or `@gate FIXME` if no condition explains it)            |
| `if (isNextDev) { test('skipped in dev mode', () => {}); return }`     | `// @force-gate prefetching` (or `!dev`) on the `describe`                  |
| `(flagEnabled ? describe.skip : describe)(...)` keyed on `process.env` | `// @force-gate <cond>` (lazy) on the `describe`                            |
| Duplicating a fixture directory per flag state                         | one fixture keyed on `__NEXT_TEST_AXIS` + a `@gate`/`@force-gate`           |
| Branching expectations on `process.env.__NEXT_CACHE_COMPONENTS`        | `if (await gate((c) => c.cacheComponents))` (`gate` from `next-test-utils`) |

The skip patterns are fake-greens: nothing tells you when the bug they hide is
fixed. `@gate` still runs the body and fails the suite the day the "known
failure" starts passing, so stale workarounds get deleted instead of rotting.

## Choosing the directive

Ask what kind of difference you're encoding:

1. **A behavior change — both states assert something meaningful.** Don't
   gate the test at all: fork inside the body with the runtime `gate()` —
   same condition registry, no inversion — which pinpoints exactly what
   differs, and also covers `it.each`, where a pragma cannot attach:
   `if (await gate((c) => c.cacheComponents)) { ... } else { ... }`. It
   mirrors React's `gate(flags => ...)`; a pragma expression string works
   too (`await gate('cacheComponents && !dev')`). A suite-level pragma is
   too coarse here — it hides _what_ is different between the states.
2. **A flag that changes the behavior of existing surface**
   (`cacheComponents`, `optimisticRouting`) **and the suite is written for one
   state.** `// @gate <cond>` on the test or `describe`. The body runs; a
   false condition inverts the expectation (failure absorbed, a pass fails as
   "stale gate"). The off state fails for a meaningful reason — the behavior
   differs — so a pass is real information: the gate is stale, delete it.
3. **A new API — the off state proves nothing.** Typically
   `// @force-gate <cond>` (lazy) on the `describe`. An API that throws when
   its flag is off — or is inert, like `useOffline()`, which compiles to a
   hook that always reports online — can only fail vacuously (often slowly,
   by timing out), and browser e2e time is considerable, so skip the run
   (and the fixture build) instead of paying for it. Working example:
   `test/e2e/app-dir/use-offline/`. This is discretion, not a rule: when the
   flag changes behavior the suite can observe, the off state is meaningful
   and `@gate` buys the staleness check.
4. **`@force-gate <cond>` also when running the body is impossible**, not
   merely failing: prefetching is off in dev, deploy has no local build
   output, the fixture cannot even build under the condition.
   - Static condition (`!dev`, `bundler`…) → real Jest `○ skipped` at
     collection.
   - Lazy condition on a `describe` → the fixture **build is skipped** when
     false; tests report passed-with-`⚠ skipped by @force-gate` (Jest cannot
     skip at runtime). Build-skipping covers `start`/`dev` suites where
     `nextTestSetup` owns the build — not `skipStart` suites, not deploy.
5. Pragmas stack: a common pair is a static `// @force-gate prefetching` plus
   a lazy `// @gate <flag>` on the same `describe`.

### Is the off-state run worth its cost?

Browser e2e time is not free, so weigh what the gated-off run buys. For a
behavior flag it usually replaces a run that was already being paid for — a
fixture that pins its flags runs identically with and without the axis set,
so keying the flag on an axis converts a redundant duplicate into coverage —
and it is what proves a pass isn't vacuous: a test that passes with the
feature off wasn't testing the feature. Absorbed failures also fail fast, so
the off-state run is cheaper than it sounds. For a new API the calculus
flips: the off state can only throw, which proves nothing, so use a lazy
`// @force-gate <flag>` on the `describe` — the fixture build is skipped
too, so the off state costs almost nothing.

## Conditions

Every name in a pragma must be declared in `test/lib/gate/conditions.ts`
(typos fail the suite at collection). Two tiers:

- **static** — the run's shape: `dev`, `start`, `deploy`, `mode`, `turbopack`,
  `rspack`, `webpack`, `bundler`, `react18`, `wasm`, `ci`, plus the
  always-false `FIXME`/`TODO`. `prod` and `prefetching` are semantic aliases
  for `!dev` — prefer the name that states _why_ the suite cannot run.
- **lazy** — a predicate over the fixture's _resolved_ `next.config`
  (`cacheComponents`, `ppr`, `useOffline`, `output`, …).

Adding one is a two-line change; follow the guidance at the top of
`conditions.ts`. The rule that matters: **lazy conditions read the resolved
config, never `process.env`** — env vars don't survive config resolution
(`__NEXT_CACHE_COMPONENTS` only applies when the fixture doesn't set
`cacheComponents` itself, and resolution implies flags the fixture never
mentions).

## Pattern: cover both states of an experimental flag

Instead of pinning a flag on (which makes the plain and axis runs identical),
key it on a test axis and gate the suite. Axes are lettered (`A`, `B`, …) —
a fixed enumeration, not a boolean and not a sharding bucket. Key the flag so
it is **enabled by default** — then the suite exercises the feature in plain
local runs with no special env, and the axis run covers the off state:

```js
// next.config.js — pin every dimension except the one under test
const nextConfig = {
  cacheComponents: true,
  experimental: {
    concurrentRouterQueue: process.env.__NEXT_TEST_AXIS !== 'A',
  },
}
```

```ts
// @gate concurrentRouterQueue
it('fails loudly on link navigation', async () => { ... })
```

The plain run exercises the feature; the axis-A run covers the off state —
the gated tests are expected to fail there, and the suite fails the day they
start passing. Working example: `test/e2e/app-dir/concurrent-router-queue/`
(tests whose expectations hold in both states stay ungated). The same keying
pairs with a lazy `@force-gate` when the off state proves nothing —
`test/e2e/app-dir/use-offline/` — which skips the redundant axis run (build
included) instead of covering it. Axis `A` aliases `__NEXT_CACHE_COMPONENTS`
for now (see `scripts/run-jest.sh`) — fine, because these fixtures pin
`cacheComponents` explicitly, so that run's env default is a no-op for them.

**Keep exactly one flag varying per fixture.** A red shard must attribute to a
single dimension.

## Pitfalls

- A pragma the transform can't attach is a **hard error**: a blank line
  between pragma and `it(`, `it.each`/`it.failing`, or a pragma inside a
  JSDoc block. Prose comments must not begin with `@gate`. A pragma on a
  skipped test (`it.skip`, `xit`, …) errors as ambiguous — remove the skip or
  the pragma. A skip without a pragma is respected.
- A `describe`-level gate does not reach `it.each` tests.
- Gated-false bodies that fail by _stalling_ waste the full Jest timeout —
  and under a lazy gate they fail the suite anyway (the runtime inversion
  only absorbs thrown errors; a static gate rides Jest's native
  `test.failing`, which does absorb timeouts). Bodies that fail via `retry()`
  timeouts also make the off-state run slow; a fast first assertion is worth
  having.
- Failures cascade in the off state: an absorbed failure mid-body skips the
  body's cleanup (e.g. a browser context left offline), so later tests may
  fail for cascade reasons. Acceptable for a tripwire, but don't puzzle over
  the individual failure messages in a gated-off run.
- `afterEach` failures (e.g. redbox matchers) are not gated — only the body is.
- `jest.retryTimes(1)` on non-dev CI means a _flaky_ gated-false test passes
  whenever it happens to fail; the tripwire is only deterministic for
  deterministic tests.
- Gated titles are unchanged in the Jest output; the
  `⚠ gated test failed as expected` log line is the only signal.
- `pragma-transform.js` bails out early on files containing neither `@gate`
  nor `@force-gate` as substrings — keep both checks if you touch it.

## Verify a gated suite in every state it can run in

```sh
# plain run (flag on): expect normal passes, no warnings
NEXT_SKIP_ISOLATE=1 pnpm test-start-webpack test/e2e/app-dir/<suite>/<suite>.test.ts

# axis run (flag off): expect `⚠ gated test failed as expected (@gate …)`
__NEXT_TEST_AXIS=A NEXT_SKIP_ISOLATE=1 pnpm test-start-webpack test/e2e/app-dir/<suite>/<suite>.test.ts

# dev (static @force-gate !dev): expect `○ skipped` at collection, no fixture boot
NEXT_SKIP_ISOLATE=1 pnpm test-dev-webpack test/e2e/app-dir/<suite>/<suite>.test.ts
```

A suite with a lazy `@force-gate` on the `describe` should additionally show
`skipping build` behavior (no `next build`) in the state where the condition
is false.

Unit tests for the infrastructure itself: `pnpm test-unit test/unit/gate/`.

## Related skills

- `$flags` — adding the experimental flag itself (config-shared, schema,
  define-env)
- `$router-act` — the prefetch-timing patterns most gated suites also use

<!-- chapter:end slug=gate-tests -->

---

<!-- chapter:begin slug=insight-error-page position=21 -->

## 21. insight-error-page

- **Source:** https://github.com/vercel/next.js/blob/canary/.agents/skills/insight-error-page/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/insight-error-page/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/insight-error-page.md
- **Origin:** Credited — installed into this repository, not published from it.
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: insight-error-page
description: Write or audit an insight-kind error page for the Next.js dev overlay. Use when creating a new `errors/<slug>.mdx` page, auditing an existing one, or checking that a page matches the framework fix cards. Covers page structure, title alignment, FixCard cards with Copy prompt button, code snippets, terminology verification against canonical docs, and Vercel technical writing style.
metadata:
  internal: true
---

# Insight Error Page — Write & Audit

Write or audit an `errors/<slug>.mdx` insight-kind page that ships from this repo to `nextjs.org` and mirrors the fix-card set in the Next.js dev overlay.

> **Terminology**: the frontmatter uses `kind: insight` but the body text calls these "errors" — never "insights". Write "this error", "error pages", "dismiss the error".

## When to use this skill

- **Write mode**: "create the error page for `next-prerender-random`", "write the sync IO docs"
- **Audit mode**: "audit the blocking-prerender-dynamic page", "check the error pages match the framework"
- Any task involving `errors/*.mdx` insight pages (frontmatter has `kind: insight`)

## Source of truth chain

Every decision traces back to one of these. When in doubt, read the source — don't guess.

| What                                          | Source file                                                                                   | How to read it                                                                                       |
| --------------------------------------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| Card titles, IDs, groups, snippets, link URLs | `packages/next/src/next-devtools/dev-overlay/components/instant/instant-guidance-data.ts`     | Each `FixCard[]` array is one error family                                                           |
| Error headline (literal text user sees)       | `packages/next/src/server/app-render/sync-io-messages.ts`, `blocking-route-messages.ts`, etc. | `createSyncIOError`, `createDynamicBodyError`, etc. — the template string is the headline            |
| Existing page (content to preserve)           | `errors/<slug>.mdx` in this repo                                                              | Read the full file; relocate useful content that doesn't fit fix cards into Gotchas or Other options |
| Canonical API docs (terminology)              | `docs/01-app/` in this repo                                                                   | Cross-check every API name, directive name, and concept against the published docs                   |
| Template structure                            | This skill file (below)                                                                       | The canonical shape of the page                                                                      |
| Vercel writing style                          | The `vercel-technical-writing` skill in `vercel/front` (not present here)                     | Apply end-to-end; see "Voice and style" below for the rules condensed                                |

## Before you start

1. **Read the framework card data** for the error family you're writing. Find the matching `FixCard[]` in `instant-guidance-data.ts`. Note every card's `id`, `title`, `group`, `link`, and `snippets`.
2. **Read the factory message** that produces the dev-overlay headline. Find `createSyncIOError`, `createSyncIOClientError`, `createDynamicBodyError`, etc. The headline template (minus the `Route "..."` prefix) becomes the page `title`.
3. **Read the existing `errors/<slug>.mdx`** if it exists. Note every pattern, code example, and caveat. You must preserve all useful content — relocate it if the new structure doesn't have a 1:1 slot for it.
4. **Read the canonical docs** for every API you'll reference: `use cache`, `cacheLife`, `cacheTag`, `connection`, `Suspense`, `useEffect`, `use client`, `generateStaticParams`, etc. Use the exact terminology from the published docs.
5. **Apply Vercel technical writing style** (active voice, sentence-case headings, no banned words). The full `vercel-technical-writing` skill lives in `vercel/front`; the condensed rules below are the minimum bar.

## Page structure (mandatory)

Every page follows this exact shape. Do not add, remove, or reorder sections.

```
---
title: <literal dev-overlay headline, no period, strip Route "..." prefix>
kind: insight
---

<the Instant Navigations callout div — the styled box linking the blog post and the Ensuring instant navigations guide; copy it from any existing insight page>

<Framing, 2-3 paragraphs. Paragraph 1 opens "During <phase>, <event>" in past tense (e.g. "During prerendering, a Server Component called ..."), names the APIs, and states the mechanism or consequence. Never open a paragraph with inline code — lead with a word ("The `params` prop ..."). Later paragraphs carry the teaching and cross-link sibling pages (parallel API families + client/server counterpart) using the "For X, see Y" formula.>

## Ways to fix this

<FixCardGrid> wrapping one <FixCard /> per framework card, in framework order

## <Card 1 title>
  Choose this fix when ...
  ### Patterns
  ### Trade-off
  ### Gotchas
  (optional: ### Short-lived caches — only for cache fixes)

## <Card 2 title>
  ...

## <Card N title>
  ...

(optional: ## Other options — for useful patterns that don't map to a framework fix card but are still relevant. Examples: bridging to a different error page's fix ("Cache the value in a Server Component" on a client page, linking to the server page), upstream content from `errors/<slug>.mdx` that doesn't fit the card structure, alternative APIs that sidestep the problem entirely. Each option gets its own `###` heading with framing prose, a code snippet, and a "Learn more" link to the page that covers it in full.)

## Verifying the fix
  (canonical two paragraphs — see "Verifying the fix" rule below)

## Don't want this validation?
  (canonical opt-out block — see "Don't want this validation?" rule below.
   Exception: sync-IO pages replace this with "## Why `instant = false` doesn't clear this error",
   because the opt-out cannot suppress sync-IO aborts.)

## Related Insights
  (full list of every other insight-kind error page, current page omitted)
```

## Rules (hard requirements)

### Frontmatter

- `title` = the dev-overlay display headline, no period. That is the string the overlay shows (see the headline strings in `errors.tsx` / the factory in e.g. `sync-io-messages.ts`), with the `Route "..."` prefix stripped and any inline expression genericized (the client-hook overlay shows `` `useSearchParams()` `` inline; its docs title says "in a Client Component" instead).
- `kind: insight` — always present.

### Verifying the fix

Every page has a `## Verifying the fix` section before the opt-out block. No page has a top-level Good to know; page-specific tips go in Gotchas under the relevant fix section. Two canonical paragraphs:

1. The observable check: "After applying a fix, reload the route and confirm the page immediately paints meaningful UI, with any `<Suspense>` fallbacks covering only the regions that stream in." (navigation insights say "navigate to the route and confirm the insight no longer appears in the dev overlay and ..." instead of "reload the route"). Followed by the empty-shell caveat sentence: a boundary around the whole page body can pass validation with an empty shell.
2. The tooling paragraph: dev overlay points at the failing component; from a build, the output is more abbreviated. Run `next build --debug-prerender` for full user-frame stack traces and `next build --debug-build-paths /dashboard /settings` to iterate on specific routes. Copy the exact wording from an existing page.

State the check as what the reader sees in the browser, not as framework artifacts ("the static shell renders real content" was retired for this reason — a fallback is expected after a correct fix; the check is that it covers only the streamed region).

### `<FixCard>` cards

- Wrap all cards in a single `<FixCardGrid>` (the same component used in `docs/01-app/02-guides/instant-navigation.mdx`).
- One `<FixCard />` per framework card, in the same order as the framework `FixCard[]` array.
- `title` = card title from framework, **verbatim**. If it reads awkward as a heading, change the framework first — never the docs.
- `href` = `#` + the auto-slug of the title (e.g. "Generate on every request" → `#generate-on-every-request`). This must match what the heading auto-generates.
- `group` = card group from framework (`dynamic`, `cache`, `client`, `stream`, `defer`, `measure`, `block`, `render`, `ignore`, `upgrade`, `disable`, `static`).
- `snippets` = the same `snippets` array as the matching framework `FixCard` in `instant-guidance-data.ts`. Copy it verbatim. No description prose lives on the card — the snippets carry the visual.
- Self-close the tag (`<FixCard ... />`). The card has no children.
- **No `prompt` prop.** The "Copy prompt" button builds the prompt dynamically at click time from the page URL and the card's `title` + `href`. The agent receives a prompt that points at the rule docs and names the fix — it then reads the docs page (the same one the user is on) for every constraint and code shape. That is why this skill exists: the docs page itself **is** the prompt's source of truth.

### `## <Fix>` sections

- Heading text = card title, verbatim. Auto-slugs to the `href` above.
- Opens with: "Choose this fix when `<condition>`."
- `### Patterns` — one `####` per meaningfully different shape of the fix. Each has:
  - 1–2 sentences of plain-prose framing
  - One short, readable `jsx filename="app/..."` snippet (complete, copy-paste-ready, no `...existing code...`)
  - Optional `Learn more:` link below the snippet
- `### Trade-off` — 1 paragraph. Mandatory. Describe the trade-off **in the context of this error**, not the generic API trade-off. If the only honest trade-off is the canonical API behavior (e.g. "GSP requires a rebuild when the list changes"), keep it to one sentence and link out to the API reference. Don't repeat what the API reference page already covers.
- `### Gotchas` — bulleted list. Mandatory (at least 1 bullet).
- Optional `### Short-lived caches` subsection for cache fixes (document the 5-minute threshold).

### Code snippets

- Must be valid React. Do not show unstable APIs (random, time, crypto) inline during render in a Client Component — that causes a hydration mismatch. Defer to `useEffect` + `useState` or an event handler.
- Lazy `useState` initializers (e.g. `useState(() => someUnstableCall())`) run during SSR — warn against this in Gotchas.
- `useRef` lazy-init pattern is valid for stable IDs (initialize in a getter function, not inline). Only applicable when the value should be computed once and frozen — not when it should reflect the current moment.
- Always include `filename="app/..."` on code blocks.
- When a pattern defers rendering to after hydration (e.g. `useEffect`), the Trade-off must link to [Preventing flash before hydration](/docs/app/guides/preventing-flash-before-hydration).

### Cross-links

- Framing paragraph: link to sibling pages (client ↔ server counterpart, parallel API families).
- Gotchas: link to the `-client` page when warning about inline render in Client Components.
- Related Insights: the full list of every other insight-kind error page, current page omitted. This is an index of the Insight family, not a curated short list. Order: body errors → metadata/viewport → unstable-value errors (server then client) → navigation Insights. Do not add API references or guides to this section; those belong inline in the body where relevant.
- Every API reference and file convention must be inline-linked throughout, not reserved for the Related Insights section.
- **Cross-page pattern linking**: When a fix on one page is covered in depth on a sibling page, show only the most common pattern inline and link out to the sibling for the full set. For example, a server page's "Render on the client" fix shows one client pattern and links to the `-client` page; a client page's "Other options" section bridges to the server page's cache fix. Don't duplicate entire sections across sibling pages — keep each page lean and let the sibling be the canonical reference.
- **First-party only**: link only to `nextjs.org/docs/*`, `react.dev/*`, `developer.mozilla.org/*`, and other canonical first-party references. **Never** link to personal blogs, community write-ups, conference talks, X/Bluesky posts, GitHub gists, or any third-party source — including the page author's own blog. If a third-party post inspired a pattern, internalize the idea and write it in our own voice without citation. Sibling error pages, our own docs, and primary API specs are the only acceptable destinations.

### Terminology (verify against canonical docs)

- `use cache` directive (not `"use cache"` in prose)
- Cache Components (capitalized)
- static shell (link to `/docs/app/glossary#static-shell`)
- `instant` (not `instant`)
- `cacheLife` / `cacheTag` / `revalidateTag` / `updateTag` — use published names exactly
- `connection()` from `next/server`
- Client Component / Server Component (capitalized)
- [prerendering](/docs/app/glossary#prerendering) — always linked on first use

### Allow blocking route section (canonical pattern)

When the framework card set includes `instant = false` (group `block`), use the canonical `## Allow blocking route` section shape. All pages with this fix must match. Cross-page consistency matters — diverging from this shape produces a page that reads like an outlier.

**Intro**: One paragraph explaining what setting `instant` to `false` does and what the trade-off is. Optional second paragraph noting when this is _rarely_ the right answer (for example, on client-hook or cache fixes where a Suspense boundary is almost always feasible). Phrase the rarity directly.

**Patterns**: For page-body errors (runtime data, uncached data, client hooks), use both `#### Opt the page out` and `#### Opt the layout out`. For viewport errors, use only `#### Opt the layout out` (viewport always lives on a layout). Each pattern has:

- 1–2 sentences of framing explaining when to use that scope
- A `jsx filename="app/..."` snippet showing the export
- A `Learn more:` link

After the pattern snippets, include a "Use either pattern when:" bulleted list (2 bullets: layout-shell-not-meaningful + incremental migration; phrase singular for viewport pages with one pattern) and a single-sentence "Don't use this to dismiss the error. Choose [Sibling fix A](#anchor-a) or [Sibling fix B](#anchor-b) when either is feasible." closer.

**Trade-off**: One paragraph. "Navigations to this route are not instant. The user waits for the full server render before any HTML arrives. Use this only when that latency is the deliberate cost of the route's purpose."

**Gotchas** (mandatory bullets, in this order):

- Setting `instant` to `false` opts out only the segment that exports it. Descendant segments remain validated by their own config or the global default.
- This export does not disable prerendering. The route still prerenders if it can. It only disables instant-navigation validation for the route.
- Page-specific gotchas (for example, viewport pages add framework-synthesized routes gotcha) come after the two canonical bullets.

**Never** add a gotcha that says `Confirm with the user that ...` in user-facing body prose. The page is what the user reads — write for them, not for the agent. Guardrails the agent should apply belong in the actual code-shape guidance under the `### Patterns` heading (which the agent reads via the docs link in the copied prompt).

### Don't want this validation?

Every insight page ends (just before `## Related Insights`) with the canonical opt-out block — except the six sync-IO pages (random/current-time/crypto and their `-client` variants), which replace it with `## Why \`instant = false\` doesn't clear this error`, because the sync-IO abort happens in the prerender path and the opt-out cannot suppress it. It teaches the reader how to opt out of validation per-segment, subtree-wide, and app-wide, since instant-navigation validation runs by default in Cache Components apps. Copy verbatim:

```mdx
## Don't want this validation?

Instant-navigation validation runs by default in [Cache Components](/docs/app/api-reference/config/next-config-js/cacheComponents) apps and is what surfaces this error.

- **One segment**: add [`export const instant = false`](/docs/app/api-reference/file-conventions/route-segment-config/instant) to the page or layout file. This opts out the segment itself. Child segments are still validated during client navigations.
- **Entire app**: set [`experimental.instantInsights.validationLevel`](/docs/app/api-reference/file-conventions/route-segment-config/instant#configuring-validation-defaults) to `'manual-warning'` in `next.config`. This limits validation to segments that explicitly export `instant`.

See [Ensuring instant navigations](/docs/app/guides/instant-navigation) for the full model.
```

### Writing style

- Lead each section with the answer: "Choose this fix when ..."
- Sentence-case headings, no periods
- No em-dashes for emphasis
- No banned words: `easy`, `quick`, `simple`, `just`, `very`, `basically`, `obviously`, `utilize`, `facilitate`, `leverage`, `robust`, `seamless`, `cutting-edge`, `innovative`
- No filler: `In this guide ...`, `As mentioned above ...`, `Let's take a look at ...`, `It's worth noting ...`
- Active voice + direct address: "You wrap the component" not "the component is wrapped"
- No "Default." labels on patterns (removed during review — patterns don't have a default)
- No semicolons in prose — split into two sentences, or use ", and" for an elliptical contrast
- Never open a paragraph or sentence with inline code — lead with a word ("The `params` prop ...")
- Code in headings is fine only when it names a real API with its exact casing (`await connection()`, `cacheLife`); concepts stay prose ("Opt the page out")
- `Learn more:` link text = the target page's exact title for guides ("Streaming", "Ensuring instant navigations"), the bare code name matching the doc title for API references ([`connection`], [`io`], [`searchParams`] — no parens); third-party APIs keep their canonical spelling ([`performance.now()`])

## Audit checklist

When auditing an existing page, check every item:

- [ ] `title` = overlay display headline, no period, inline expressions genericized
- [ ] `kind: insight` in frontmatter
- [ ] No top-level Good to know; `## Verifying the fix` present with the two canonical paragraphs (observable check + `--debug-prerender` tooling)
- [ ] All cards wrapped in a single `<FixCardGrid>`
- [ ] One `<FixCard />` per framework card, in framework order
- [ ] Every `<FixCard />` `title` = card title verbatim
- [ ] Every `<FixCard />` `href` = `#` + auto-slug of the heading
- [ ] Every `<FixCard />` `group` matches framework card group
- [ ] Every `<FixCard />` `snippets` = the framework card's `snippets` array, verbatim
- [ ] No `prompt` prop on any `<FixCard />` — the copy button generates the prompt from `title` + `href` + the page URL
- [ ] `<FixCard />` is self-closing (no children, no description prose)
- [ ] Every `## <Fix>` heading = card title verbatim
- [ ] Every fix section has `### Patterns`, `### Trade-off`, `### Gotchas`
- [ ] No "Default." labels on patterns
- [ ] No `Confirm with the user ...` phrasing anywhere in the page. The page is for the user; the agent reads the same page via the docs link in the copied prompt.
- [ ] If the page has `## Allow blocking route`, it matches the canonical shape: patterns (page-body errors use both Opt the page out + Opt the layout out; viewport errors use Opt the layout out only), "Use either pattern when" list, "Don't use this to dismiss the error" closer, canonical 2-bullet Gotchas
- [ ] Code snippets are valid React (no inline `Math.random()` during render in Client Components)
- [ ] `useState(() => Math.random())` warned against in Gotchas
- [ ] All API references inline-linked throughout
- [ ] Sibling pages cross-linked in framing paragraph (inline body links carry the bulk of API references)
- [ ] `## Don't want this validation?` present, verbatim per the canonical block (sync-IO pages instead have `## Why \`instant = false\` doesn't clear this error`)
- [ ] `## Related Insights` section present, listing every other insight-kind error page (current page omitted)
- [ ] Upstream `errors/<slug>.mdx` content preserved (relocated to Gotchas or Other options if needed)
- [ ] Terminology matches canonical docs (verified, not assumed)
- [ ] Vercel technical writing style applied (no banned words, active voice, sentence-case headings)
- [ ] Framework card `link` URLs point to the correct heading auto-slugs (if not, flag as a framework follow-up)
- [ ] Short-lived caches subsection present under cache fixes (when applicable)
- [ ] No prose semicolons; no paragraph opens with inline code
- [ ] `Learn more:` texts follow the title/bare-API convention and every target is the best page for the pattern

## File locations

- New pages: `errors/<slug>.mdx` (this repo)
- URL: `https://nextjs.org/docs/messages/<slug>`
- `nextjs.org` clones `errors/` from canary on every deploy (sync pipeline lives in `vercel/front`)
- Framework cards: `packages/next/src/next-devtools/dev-overlay/components/instant/instant-guidance-data.ts`
- Factory messages: `packages/next/src/server/app-render/sync-io-messages.ts`, `blocking-route-messages.ts`, `use-cache-messages.ts`

## Reference page

The canonical reference page is `errors/blocking-prerender-random.mdx`. When writing a new page, read it first to match the exact structure, tone, and level of detail.

<!-- chapter:end slug=insight-error-page -->

---

<!-- chapter:begin slug=next-rspack position=22 -->

## 22. next-rspack

- **Source:** https://github.com/vercel/next.js/blob/canary/.agents/skills/next-rspack/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/next-rspack/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/next-rspack.md
- **Origin:** Credited — installed into this repository, not published from it.
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: next-rspack
description: >
  Maintain @next/rspack-core and @next/rspack-binding packages. Use when editing
  rspack/package.json, rspack/crates/binding/Cargo.toml, rspack/rust-toolchain.toml,
  or packages/next-rspack/package.json. Covers upgrading @rspack/core npm version,
  rspack_* crate versions, Rust toolchain version, building and linking for local
  testing, and NEXT_RSPACK environment variable usage. Does NOT apply to root
  rust-toolchain.toml (that's for Turbopack).
metadata:
  internal: true
---

# Next.js with Rspack

## When to Use

Use this skill when you need to:

- Upgrade `@rspack/core` npm package version
- Upgrade rspack crate dependency versions
- Upgrade Rust version in `rspack/rust-toolchain.toml` (rspack directory only)
- Fix Rspack-related compilation issues
- Develop Rspack-specific features in Next.js

> **⚠️ Scope Limitation**: This skill only applies to code and configuration under the `rspack/` directory. The `rust-toolchain.toml` in the repository root is for Turbopack and is outside the scope of this skill.

## Architecture Overview

### Package Structure

```
rspack/                          # Independent workspace (separate rust toolchain and npm releases)
├── Cargo.toml                   # Rust workspace configuration
├── rust-toolchain.toml          # Rust version configuration
├── package.json                 # @next/rspack-core package definition
├── lib/
│   ├── index.js                 # Exports @rspack/core + custom plugins
│   └── index.d.ts               # Type definitions
└── crates/
    └── binding/
        ├── Cargo.toml           # Rust crate dependencies (rspack_* crates)
        └── package.json         # @next/rspack-binding package definition

packages/
└── next-rspack/
    └── package.json             # References @next/rspack-core
```

### Dependency Graph

```
packages/next-rspack
    └── @next/rspack-core (rspack/package.json)
            ├── @rspack/core (npm dependency)
            └── @next/rspack-binding (rspack/crates/binding)
                    └── rspack_* crates (Cargo.toml dependencies)
```

### Version Mapping Rules

| @rspack/core npm version | rspack crate version | Notes                                             |
| ------------------------ | -------------------- | ------------------------------------------------- |
| `2.0.0-rc.0`             | `0.100.0-rc.0`       | npm major.minor maps to crate 0.(major\*50+minor) |
| `1.3.x`                  | `0.53.x`             | e.g., 1.3 → 0.53                                  |
| `1.2.x`                  | `0.52.x`             | e.g., 1.2 → 0.52                                  |

## Upgrade Process

### Step 1: Get Upstream Version Information

Query https://github.com/web-infra-dev/rspack to obtain:

- Target `@rspack/core` npm version
- Corresponding rspack crate version
- Rust channel version from `rust-toolchain.toml`

### Step 2: Update npm Dependencies

Edit `rspack/package.json`:

```json
{
  "dependencies": {
    "@rspack/core": "<new version>",
    "@next/rspack-binding": "workspace:*"
  }
}
```

### Step 3: Update Cargo.toml

Edit `rspack/crates/binding/Cargo.toml`, update all rspack crate versions:

```toml
[dependencies]
rspack_binding_builder        = { version = "=<new crate version>" }
rspack_binding_builder_macros = { version = "=<new crate version>" }
rspack_core                   = { version = "=<new crate version>" }
rspack_error                  = { version = "=<new crate version>" }
rspack_hook                   = { version = "=<new crate version>" }
rspack_plugin_externals       = { version = "=<new crate version>" }
rspack_regex                  = { version = "=<new crate version>" }
# rspack_sources version is managed separately, may not follow the main version

[target.'cfg(...)'.dependencies]
rspack_binding_builder = { version = "=<new crate version>", features = ["plugin"] }

[build-dependencies]
rspack_binding_build = { version = "=<new crate version>" }
```

### Step 4: Update Rust Toolchain

Edit `rspack/rust-toolchain.toml`:

```toml
[toolchain]
profile = "default"
components = ["rust-src"]
channel = "<nightly version matching upstream rspack>"
```

### Step 5: Verify Build

```bash
# Execute in rspack directory
cd rspack

# Check Rust code
cargo check

# Build binding (requires Rust installed)
pnpm build
```

### Step 6: Link to packages/next-rspack

`rspack/` is an independent workspace not included in the root pnpm-workspace, manual linking is required:

```bash
# Execute from repository root
cd packages/next-rspack

# Link locally built @next/rspack-core
pnpm link ../../rspack
```

Or modify `packages/next-rspack/package.json` to use local path (for local testing only, do not commit):

```json
{
  "dependencies": {
    "@next/rspack-core": "link:../../rspack"
  }
}
```

> **Important**: After linking, run `pnpm install` in the root directory to update dependency relationships.

### Step 7: Run Tests

```bash
# Execute from repository root
pnpm test-rspack
```

## Key Files Checklist

Files to check/modify during upgrade:

| File Path                            | Modification                                                                          |
| ------------------------------------ | ------------------------------------------------------------------------------------- |
| `rspack/package.json`                | `@rspack/core` version, package version                                               |
| `rspack/crates/binding/Cargo.toml`   | rspack\_\* crate versions                                                             |
| `rspack/crates/binding/package.json` | binding package version                                                               |
| `rspack/rust-toolchain.toml`         | Rust nightly version                                                                  |
| `packages/next-rspack/package.json`  | `@next/rspack-core` version reference (update on release, use link for local testing) |

## Pre-requisites for Local Testing

Before running `pnpm test-rspack`, you must build and link `@next/rspack-core`:

```bash
# 1. Build @next/rspack-core
cd rspack
pnpm install
pnpm build

# 2. Link to packages/next-rspack
cd ../packages/next-rspack
pnpm link ../../rspack

# 3. Return to root and update dependencies
cd ../..
pnpm install

# 4. Run tests
pnpm test-rspack
```

> **Note**: After modifying Rust code under `rspack/`, you need to re-run `pnpm build` from step 1.

## Test Commands

```bash
# Full test suite (using Rspack compiler)
pnpm test-rspack

# Development mode tests
pnpm test-dev-rspack

# Production mode tests
pnpm test-start-rspack

# Run specific tests only
pnpm run with-rspack pnpm testonly -- <test-pattern>
```

All test commands set environment variables via `with-rspack`:

```bash
cross-env NEXT_RSPACK=1 NEXT_TEST_USE_RSPACK=1
```

## Development Notes

### Environment Variable Differentiation

Use the `NEXT_RSPACK` environment variable to differentiate compilers in `packages/next/` code:

```typescript
// Check if using Rspack
const isRspack = Boolean(process.env.NEXT_RSPACK)

if (process.env.NEXT_RSPACK) {
  // Rspack-specific logic
} else {
  // Webpack logic
}
```

### Common Code Locations

- Compiler selection logic: `packages/next/src/lib/bundler.ts`
- Webpack configuration: `packages/next/src/build/webpack-config.ts`
- Loader adaptations: `packages/next/src/build/webpack/loaders/`

### Custom Plugins

`@next/rspack-core` exports = `@rspack/core` + custom plugins:

- `NextExternalsPlugin` - externals handling plugin implemented in Rust

## Release Process

Release via GitHub Actions workflow:

- Workflow: `.github/workflows/release-next-rspack.yml`
- Supports dry-run mode
- Supports multiple npm tags (latest, alpha, beta, canary)

## References

- Upstream Rspack repository: https://github.com/web-infra-dev/rspack
- Rspack documentation: https://rspack.rs
- Next.js with Rspack example: `examples/with-rspack/`

<!-- chapter:end slug=next-rspack -->

---

<!-- chapter:begin slug=react-sync position=23 -->

## 23. react-sync

- **Source:** https://github.com/vercel/next.js/blob/canary/.agents/skills/react-sync/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/react-sync/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/react-sync.md
- **Origin:** Credited — installed into this repository, not published from it.
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

Bundled files (1), referenced from this skill's directory:
  - `scripts/build-for-next.sh` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/react-sync/scripts/build-for-next.sh

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: react-sync
description: >
  Build local React changes in the bundle variants consumed by Next.js, sync
  them into a local Next.js checkout, and test the resulting integration. Use
  when working on React changes that need validation in Next.js, or when asked
  to run buildForNext, pnpm sync-react, or synchronize a React checkout with
  Next.js.
metadata:
  internal: true
---

# Sync React to Next.js

Use this skill when syncing a local React checkout into this Next.js checkout.

## Build and sync

1. Build React from its own checkout. Pass this skill's script by its absolute
   path, because the working directory must be the React repository.

   ```bash
   cd <react-repo>
   bash <next-repo>/.agents/skills/react-sync/scripts/build-for-next.sh
   ```

   This creates `build/oss-stable` and `build/oss-experimental` with the
   bundle variants that Next.js consumes.

2. Sync the build into Next.js:

   ```bash
   cd <next-repo>
   PATH="$(dirname "$(command -v corepack)"):$PATH" \
     pnpm sync-react --version <react-repo>
   ```

   Work around a Codex `PATH`-precedence bug by keeping the directory containing
   the active Corepack shim first on `PATH`. Codex injects a separate bundled
   `pnpm` executable ahead of the user's Corepack shim, and command lookup
   happens before Corepack reads the repository's `packageManager` field. The
   prefix makes both this command and `sync-react`'s nested `pnpm install`
   resolve through Corepack to the repository-pinned pnpm version. Invoking
   only the outer command with `corepack pnpm` is insufficient because the
   nested command still resolves `pnpm` from `PATH`.

3. Inspect the sync result before testing. Preserve unrelated changes in both
   checkouts. Rebuild Next.js when required, then run the focused test command
   matching the changed behavior.

## Related Skills

- `$react-vendoring` - vendored React runtime and type boundaries after syncing.

<!-- chapter:end slug=react-sync -->

---

<!-- chapter:begin slug=sandbox-bench position=24 -->

## 24. sandbox-bench

- **Source:** https://github.com/vercel/next.js/blob/canary/.agents/skills/sandbox-bench/SKILL.md
- **Raw:** https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/sandbox-bench/SKILL.md
- **Markdown:** https://skillsdocs.com/vercel/next.js/sandbox-bench.md
- **Origin:** Credited — installed into this repository, not published from it.
- **Licence:** MIT — https://spdx.org/licenses/MIT.html

Bundled files (12), referenced from this skill's directory:
  - `references/methodology.md` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/sandbox-bench/references/methodology.md
  - `scripts/bench-analyze.mjs` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/sandbox-bench/scripts/bench-analyze.mjs
  - `scripts/bench-collect.mjs` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/sandbox-bench/scripts/bench-collect.mjs
  - `scripts/bench-common.mjs` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/sandbox-bench/scripts/bench-common.mjs
  - `scripts/bench-db.mjs` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/sandbox-bench/scripts/bench-db.mjs
  - `scripts/bench-stats.mjs` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/sandbox-bench/scripts/bench-stats.mjs
  - `scripts/bench-status.mjs` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/sandbox-bench/scripts/bench-status.mjs
  - `scripts/config.mjs` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/sandbox-bench/scripts/config.mjs
  - `scripts/sandbox-e2e.mjs` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/sandbox-bench/scripts/sandbox-e2e.mjs
  - `scripts/sandbox-gate.mjs` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/sandbox-bench/scripts/sandbox-gate.mjs
  - `scripts/sandbox-ssr.mjs` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/sandbox-bench/scripts/sandbox-ssr.mjs
  - `scripts/sandbox-sweep.mjs` — https://raw.githubusercontent.com/vercel/next.js/canary/.agents/skills/sandbox-bench/scripts/sandbox-sweep.mjs

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: sandbox-bench
description: >
  Benchmark React or Next.js changes on Vercel Sandbox VMs with paired
  A/B statistics: react PR/commit vs base, or Next.js PR/commit vs base,
  measured end-to-end through the bench/render-pipeline app (rps,
  latency, p95; TTFB, RSS and document/Flight bytes when the Next
  side captures them) and, for React changes, through the react
  repo's flight-ssr-bench fixture (Node AND Edge web-streams paths,
  Fizz and Flight+Fizz). Use whenever the user asks to bench, perf
  test, or A/B a React PR, a react-server-dom / Flight / vendored React
  change, or a Next.js PR ("is this PR faster", "does this regress
  RSC?", "measure the perf impact of <commit>"), even if they don't say
  "benchmark" — any request to quantify a server-side performance
  difference between two revisions belongs here. Runs remotely
  (laptop-free), applies correctness gates before measuring, and
  reports boot-level confidence intervals.
metadata:
  internal: true
---

# Sandbox bench: paired A/B perf runs for React and Next.js changes

Measures what a change is actually worth, end to end: two revisions
("arms") built into otherwise-identical Next.js apps, exercised by the
`bench/render-pipeline` harness on Vercel Sandbox VMs, compared with
paired statistics that treat the VM boot as the unit of replication.
All heavy work happens on sandbox VMs; the laptop only orchestrates.

Scripts live in `scripts/` next to this file and run from anywhere.
Arms are git refs, resolved in cached clones of react and next.js;
the Next side defaults to canary. Everything is cached
content-addressed: first use of a new pair builds caches (~45-60 min
extra, once); later runs boot straight into measurement.

## One-time setup

1. `node scripts/config.mjs show` — if it reports NOT CONFIGURED, ask
   the user which Vercel **team** and **project** the sandbox VMs
   should run under (these are billed resources; never guess, never
   default), then `node scripts/config.mjs set team=<slug> project=<name>`.
   Config lives in `~/.config/sandbox-bench/config.json` — never commit
   team/project names into the repo.
2. The Vercel CLI session must have access to that team. On a 403,
   stop launching (don't retry through it) and check whether access is
   already back: `vercel whoami --scope <team-slug>` plus one scoped
   read call (e.g. `vercel sandbox ls`) — grants drop and recover on
   their own, and a transient 403 needs no login at all. If
   verification still fails, run `vercel login <team-slug>` yourself
   as a background task (the token lives with the CLI session, not
   with the user). It opens a browser/device confirmation — relay the
   URL if one is printed — but keep re-running the verification pair
   every minute or two while it waits: access often returns before
   the login flow reports success, and once verification passes, kill
   the pending login and resume. After a 403 outage, expect in-flight runs to have died:
   run `node scripts/bench-status.mjs` and follow its recovery
   actions (measurement VMs will have hit their ~5h timeout if the
   outage was long — those cells need relaunching, not collecting).
3. react and next.js clones land in the cache on first use (or point
   `reactRepo`/`nextRepo` in the config at existing checkouts).

Before the first real run with a new configuration, sanity-check the
plan with `--dry-run` (prints what would happen, touches nothing).

## Workflow

### 1. Resolve what's being compared

- **React PR**: `--pr <url|number>` — base is computed automatically
  (merge-base of the PR head with react main).
- **React refs**: `--arms base=<ref>,cand=<ref>` — base FIRST. For a
  multi-commit branch, base is the merge-base with main, not `cand^`.
- **Next.js PR**: `--next-pr <url|number>`. The React side defaults to
  whatever each Next ref vendors (that's what would ship); pass
  `--react-ref` only to pin both arms to one specific React build.
- **Next refs**: `--next-arms base=<ref>,cand=<ref>`.

Exactly one side varies; the other is identical in both arms. That
isolation is what makes the numbers attributable — never vary both.

### 2. Gate correctness before spending bench compute

A bench number from an arm that fails its own tests is meaningless.
For any arm that is not already CI-green upstream (hand-assembled
branches, cherry-picks with resolved conflicts, local commits):

```sh
node scripts/sandbox-gate.mjs --arms cand=<ref>
```

The bench itself enforces the primary gate: every react arm's commit
must have green CI on the react repo, checked automatically before any
build or VM is spent. PRs and main-history commits normally satisfy
this with no extra work. For local or unpushed refs (no CI exists),
gate on a VM with sandbox-gate.mjs and then pass --allow-ungated to
the bench. The VM gate runs the full test suite in prod mode (the
channel that gets benched). PASS requires seeing the actual test
counts in the output. If a gate fails, report the failures and stop —
do not bench a broken arm. Each arm is gated in its own
lockfile's environment. Bench the exact sha the gate prints (a branch
ref can move between gate and bench).

### 3. Launch the bench (background, non-blocking)

```sh
bash -c 'node scripts/sandbox-e2e.mjs --pr <url> --label <slug> \
  2>&1 | grep --line-buffered -v "^live "; exit ${PIPESTATUS[0]}'
```

For React PRs, launch BOTH suites (separate background tasks; they
share arm builds and caches):

```sh
bash -c 'node scripts/sandbox-ssr.mjs --pr <url> --label <slug>-ssr \
  2>&1 | grep --line-buffered -v "^live "; exit ${PIPESTATUS[0]}'
```

The e2e suite measures the Node path through a real Next.js app; the
ssr suite measures the react repo's flight-ssr-bench fixture — 8
variants (Fizz and Flight+Fizz, Node and Edge web streams, sync and
async), each sequentially with Flight script injection and behind an
HTTP server at c=1/c=10. Edge cells are the ssr suite's headline (the
e2e suite cannot see that path); its Node and Fizz-only cells
attribute an effect to the Flight layer, the Fizz layer, or the
stream plumbing. The fixture (the workload) is pinned to one ref for
both arms — react main by default — so only the React builds differ;
if the PR itself edits the fixture, the launcher says so and the run
does not measure those edits. Next PRs run the e2e suite only.

- Run it as a background task and proceed on its completion
  notification. Never hold a foreground wait; never poll in a loop —
  the rule is about control flow, not status relay: reading the
  output tail to answer "how's it going" is always fine.
- The harness handles the invariants internally: both arms in the same
  VM, interleaved ABBA, paired per (vm, run); detached remote
  execution (transport drops don't kill runs); build fingerprints
  recorded in every result row.
- `live ...` lines are streaming estimates for progress display only.
  Never stop a run early because a live p-value looks good, and never
  report a live number — sequential peeking manufactures false
  positives. Only the final analysis counts.
- Defaults (16 VMs × 2 paired runs) implement the methodology; don't
  reduce VM count to save time — boots are the unit of inference, and
  fewer boots means wider intervals, not faster answers.
- Sandbox compute is internal capacity, not a budget: launch, relaunch,
  and confirm runs without asking about cost or shrinking them to
  save it.
- The Next side's default, `canary`, is the latest published canary
  release (the launcher prints its version and sha), so repeat benches
  reuse the built snapshot until a new canary ships.
- Useful flags: `--bench-env KEY=VALUE` (runtime-only env for the
  bench process — it does NOT affect the snapshot's app build), `--isolate-routes`
  (tail investigations), `--no-profile` (skip the CPU capture that
  runs by default after the timed runs), `--prepare` (build caches
  only — use when two cells will share an arm, to avoid duplicate
  builds racing).
- CPU profiles are captured by default: one profile pass per arm runs
  strictly AFTER the timed runs (it cannot touch the numbers), costs
  ~45-60 min extra VM wall-clock, and lands in `<runDir>/prof-vm<N>/`
  as standard V8 `.cpuprofile` files. Cross-VM profile diffs are
  highly stable (observed 16/16 sign agreement on real movers), so one
  profiled cell suffices to rank hot paths. Analysis caveats:
  aggregate by (functionName, line, column) — bare minified names
  collide across the bundle — and never diff arms by minified name
  (the minifier renames between builds); match positions or code
  snippets instead.
- The bench exercises Next's node-streams path
  (`__NEXT_USE_NODE_STREAMS` is inlined as true for the node runtime
  at build time). React changes that only touch the EDGE stream
  configs are not exercised end-to-end and will (correctly) bench as
  no detected difference.

### 4. Read the result like a skeptical data scientist

The goal is the truth about the change, not making its author feel
good. The final analysis prints, per route/phase/metric, the
boot-level mean, ±95% CI, and p across boots. Apply the policy in
[references/methodology.md](references/methodology.md):

- Claim only boot-level p < 0.01, with the CI, on an A/A-validated
  team/config (see methodology).
- The PR is a hypothesis, not an explanation. Claims come from the
  analysis output alone. When the numbers agree with the PR's story,
  check whether the captured data actually discriminates that
  mechanism from alternatives — a latency win attributed to smaller
  payloads should come with a document-bytes delta; if the bytes
  didn't move, the story doesn't hold and the report says so.
- Use every captured metric, and voice anything that does not add up:
  one metric family moving against the others, effects with no
  byte-level or RSS trace, throughput moving without latency,
  sign flips across boots. An inconsistency you cannot explain
  belongs in the report, not in the drawer.
- The `within-run p` shown in brackets is a diagnostic, never a claim.
- Check the fingerprint header first: two distinct fingerprints = valid
  A/B; "inconsistent fingerprints" = invalid, report no numbers. The
  fingerprint hashes both bundlers' compiled server files — arms
  touching only client files can still legitimately show identical
  fingerprints with different version strings.
- Per-boot values are printed; if boots disagree in sign, say so.
- Any claim that will drive a decision gets one independent
  confirmation run before it's stated as fact.

Re-analyze any past run without re-running it:
`node scripts/bench-analyze.mjs <runDir>`.

### 5. Report

Name what was measured with links: the PR title (printed in the
analysis header, stored in meta.json) linking to the PR; for ref
arms, the commit title. Lead with a table of the significant cells,
each row carrying the effect with its unit, the CI, and p:

```
## [<PR title>](<PR url>) — e2e, Vercel Sandbox (x86 Xeon), <n> boots

Significant (boot-level p < 0.01, A/A-validated):
| cell | effect | 95% CI | p |
|---|---|---|---|
| /dashboard under load | +14.4% throughput (req/s) | ±3.2% | <0.0001 |
| /dashboard serial | −10.7% median latency (ms) | ±0.6% | <0.0001 |

No detected difference: <every cell not in the table, by name>.
Flags: <cells at 0.01 ≤ p < 0.05, sign disagreements across boots,
fingerprint caveats, anything that does not add up>
```

One row per cell: rps and median restate each other, so report the
throughput number (add a p95 row only when the tail moves differently
from the median). Document metrics (raw/gzip/Flight KB) get their own
rows when they differ — they are the mechanism evidence. When the
Next side predates the document-metrics harness (vercel/next.js#95828)
those cells are absent; say so instead of silently reporting less. State the platform next to the numbers. Magnitudes
are platform-dependent (GC share differs by CPU); direction and
mechanism transfer, percentages do not. Never present a
noise-compatible delta as a small win or loss — it is "no detected
difference".

## Results database

Every collected run lands in one SQLite file,
`~/.cache/sandbox-bench/results.db` — raw measurements and artifacts
(CPU profiles, logs) only, written exclusively by the importer, never
by hand. The launcher imports and verifies automatically at
collection; `bench-analyze` reads the db and nothing else, so every
statistic is a pure function of it. Numbers in reports come from the
analysis output verbatim — never retype, recompute, or aggregate them
yourself.

- `node scripts/bench-db.mjs ls` — all runs with sample/artifact counts.
- `node scripts/bench-db.mjs verify [runId]` — integrity checks:
  sqlite-level, referential, one fingerprint per arm, paired sample
  counts, artifact sha256. Run it before drawing on old data.
- `node scripts/bench-db.mjs export out.db <runId...>` — cut a
  self-contained db of specific runs (with their profiles) to send to
  someone. It opens in any SQLite tool.
- `node scripts/bench-analyze.mjs <runId>` — re-analyze anything in
  the db; a run-dir argument imports it first.

## Keeping the user informed

The launcher narrates itself on stdout: launch facts first (run dir,
arms, CI verdicts), then a progress line every ~2 minutes with rows
collected and interim per-route effects with confidence. Relay to the
user: the run dir and expected duration right after launching,
notable interim shifts if they ask how it's going, and the full
verdict from the final analysis when the completion notification
arrives. The analysis names metrics that were `not captured on this
run` — repeat that in the verdict when it limits what the data can
say (document metrics absent means the payload mechanism is
unverified, not verified-identical).

While a run is active, open any reply with a one-line status per run:
read the tail of the launcher's output and quote its latest progress
line. If the session supports timed wakeups or reminders, schedule a
check at each expected transition (arm builds -> experiment snapshot
-> measuring, then every ~15 minutes of measurement) and post the
progress line; if not, say when the next update will arrive so
silence is never ambiguous. Interim effects in progress lines are
streaming estimates — share them as progress, never as claims.

If a launcher process dies (session teardown, crash), the remote VMs
keep executing their measurement loops — the data is not lost. `node
scripts/bench-collect.mjs <runDir>` reconnects, waits for the loops,
downloads the results, cleans up, and analyzes. Run it before the VMs
hit their ~5h timeout.

## Failure recovery

- **First move, always: `node scripts/bench-status.mjs`.** Session
  restarts silently kill background launchers while their detached VMs
  keep measuring, and a dead launcher's log still ends with a
  healthy-looking progress line — never infer liveness from log tails
  or task output files. bench-status checks each run's recorded
  launcher pid and prints the per-run recovery action (running /
  collect now / relaunch). Run it at the start of any session that
  expects work in flight, after any crash, and before telling the user
  what is or isn't running. Launcher crashes are also recorded in the
  run's status.json (`phase: "failed"` plus the error).
- **Interrupted local process**: remote VMs keep running detached.
  `vercel sandbox list` (with the configured team/project) to find
  them; poll each VM's `/vercel/sandbox/loop.done`, `cp` its
  `results.jsonl` down when done, then remove the VM and analyze with
  `bench-analyze.mjs`.
- **Leaked VMs** after any crash: `node scripts/sandbox-sweep.mjs`
  lists this skill's VMs (matched by sbench-\* name AND the
  purpose=sandbox-bench tag, and only when older than --min-age-hours,
  default 3, so healthy in-flight runs are never touched); `--yes`
  removes them by exact listed name.
- **Flaky uploads/transports**: the harnesses size-check artifacts and
  abort on truncation. A failed cell is safe to relaunch; caches make
  the retry cheap. Don't relaunch two cells that need the same uncached
  arm at the same moment — they'll race to build it; use `--prepare`
  first instead.

## Cost expectations (set these with the user before big runs)

Per cell at defaults: ~18 VMs (8 measurement + build/snapshot VMs),
~1-2h wall-clock cold, ~30-60 min warm. A/A calibration and
confirmation runs are extra cells. VMs are billed to the configured
team — for anything beyond a single PR check, confirm scope first.

<!-- chapter:end slug=sandbox-bench -->
