next-cache-components-optimizer

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.

Install
npx skills add 'https://github.com/vercel/next.js/tree/canary/skills/next-cache-components-optimizer'
Download bundle ↓
canary · bfcf687Scanned 2026-09-15

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗
View on GitHub
← Back to SKILL.md

RED-test robustness: verify the RED before optimizing

The C-gate of the workflow. A RED that is red for the wrong reason sends you optimizing a route that was never broken: the route becomes instant, the test stays red (or the code is contorted to satisfy a broken assertion), and the effort lands on the wrong problem. The prevention is cheap: spend a few minutes verifying the RED is trustworthy first.

The deciding question

Does the marker render WITHOUT the lock, as the test user?

  • No: the test is red because the marker or page is not there for that user or environment. A marker bug, not an instant-navigation bug. Fix the marker. (This is the most common case.)
  • Yes: the marker exists and is reachable; a red under the lock is a genuine "not instant". Optimize the route.

Everything below serves answering that question honestly.

When the blocked route will not build

With Cache Components, a top-level blocking read can fail the build before the test can produce a RED. Add export const instant = false to the target route as part of the temporary RED scaffold. It lets the known blocker build without making the navigation instant. Remove the opt-out with the fix, and include it when reverting the fix for the differential.

Do not manufacture a RED with cookies() or a session read alone. The testing lock restricts the navigation to its shell; it does not make request cookies unavailable. Use the route's real blocking uncached data, and prefer the self-validating test variant when deferred content exists.

The robustness checklist (all must hold)

  1. Red on baseline: fails on the unfixed route.
  2. Right reason: without the lock, the marker is visible on the production-build rig (never next dev), running as the test user, not only in the author's own logged-in session.
  3. Differential: reverting only the fix → RED; re-applying → GREEN; nothing else moves it.
  4. Non-gameable marker: a sync element of the static shell, never streamed data.
  5. Deterministic on a production build: stable across N runs; never next dev.
  6. Discriminating both ways: present under the lock on an instant route, absent under the lock on a blocking one.
  7. Renders for the test user: under that user's flags, plan, role, and data.
  8. Conditional redirects accounted for: assert at the route's real destination for that user.
  9. Real selector: a data-testid on a known static-shell node, not a guessed role/name.
  10. Visible marker: not display:none, off-screen, or inside a hover overlay; for lists, target .filter({ visible: true }).first().
  11. Fresh build under test: the deployment being measured contains the latest commit, not a build URL still serving the previous deploy.

Taxonomy: red for the wrong reason

Any of these makes a RED untrustworthy. None of them is "the navigation isn't instant."

Wrong reasonHow it occursHow to rule it out
Selector matches nothinga guessed getByRole('button', { name: 'Folder' })grep the component for the real accessible name; add a data-testid
Conditional redirectthe route redirect()s for the test user (flag/role), so the marker page is never reachedcheck the page's top-level branches; assert at the real destination, or pin the flag
Flag / plan / role gatethe author has the flag or plan; the test user does notrun the unlocked baseline as the test user; pin flags via the project's override mechanism
Empty statethe marker only exists when data does; the CI account is emptypick a marker present in the empty state (a layout element such as the page header), or seed data
Timeout / flakea slow API or transient infrastructure errorre-run; separate infrastructure flake from a real signal
Streamed markerthe marker is behind <Suspense>, so it is never in the shellchoose a sync shell element; verify it sits outside every <Suspense>
Auth redirectunauthenticated → /loginconfirm login succeeded before the navigation
Stale deploymentthe test ran against the previous build (the URL under test still serves the prior deploy)poll the deployment for a marker from the latest commit before trusting any verdict
Hidden / off-screenthe testid is on a hover-overlay or off-screen list itemput the marker on an always-visible node; .filter({ visible: true }).first() for lists

Worked cases

These are illustrative failures from real optimization runs; each was red for a wrong reason, and none was an instant-navigation problem. One app's drift surface might be dominated by feature flags and plans; another's by auth state, an empty database, or locale. The taxonomy lists every wrong reason; the rig file's DRIFT list says which rows apply to your app.

  • Guessed selector + empty state: the marker was a button picked by a guessed accessible name that no element actually had, on a list page whose CI account had no rows. → checks 7, 9. Fix: a data-testid on a real static-shell node.
  • Hidden marker: the testid sat first on a hidden sm:block hover-overlay link, then on an off-screen carousel card; Playwright resolved the element but reported it hidden. → check 10. Fix: an always-visible node; for lists, .filter({ visible: true }).first().

Differential check (capture in the PR)

The strongest evidence that the RED measured the property:

1. on the fixed branch → GREEN
2. revert ONLY the fix (the <Suspense> push-down) → RED
3. re-apply → GREEN
4. confirm no other change moves it

Link the two runs (or include the toggle diff and results) in the PR description. A reviewer who sees the differential knows the test measures the property.

instant() is not a stopwatch

See: instant().

The test does not measure how fast a navigation is. instant() gates dynamic data so the content of the static shell can be asserted; the signal is presence, not speed. Under the lock, an instant route's shell is present, and a blocking route's content never commits, regardless of wait time. Therefore:

  • The shipped assertion is await expect(SHELL_MARKER).toBeVisible() under the lock. Do not add a custom timeout or a painted boolean.
  • A custom short timeout (e.g. 3000) implies a race against a clock that does not exist. It adds nothing to the verdict and invites false REDs on an instant route whose commit lands a microtask late.
  • Do not use locator.isVisible({ timeout }) as a soft wait: Playwright deprecated and ignores that timeout; the call returns immediately.
  • "Renders for the test user" (checks 7-9) is established at authoring time with the unlocked baseline scaffold, not by a timed assertion in the shipped test.

instant() guards need no retries and no prefetch warming

An instant() guard is deterministic. Do not configure retries on one, and do not hover-warm to help a prefetch land in time. Under the lock, the router initiates the route prefetch and awaits it before committing (even for a prefetch={false} link, even for a route already in the prefetch cache), so the committed shell does not depend on any prior render, hover, or menu-open prefetch. A flaky guard has a real cause: a marker that is not a sync node of the destination's shell, a flag/role/empty-state gap for the test user, or a genuinely blocking route. The fix is in the page or the marker; a retry masks the regression the guard exists to catch. The only legitimate .hover()/menu-open is when the trigger element itself is not in the DOM until hovered or opened.

Silent no-op: the testing API must be exposed in the measured build

instant() works by setting a cookie (next-instant-navigation-testing) that lock code inside the build reads. It does not throw when that lock code is absent; it only throws on nested calls or an unknown base URL. If the build was produced without the testing API (experimental.exposeTestingApiInProductionBuild), the cookie is ignored, the navigation runs normally, and the instant() test passes vacuously. A green instant() test is only meaningful if the lock engaged.

Two defenses; use both:

  1. Confirm the API is exposed on the target. Wire the flag to the platform's preview/staging condition or an explicit environment variable; the rig file records the project's spelling (SKILL.md phases 0 and A). Do not trust a pass from a build where it is not set.

  2. Make the test self-validating: for any route with deferred content, also assert that the deferred content is gated under the lock, not only that the shell is present (../test-template.md, self-validating variant). If the lock did not engage, the content is already present and toHaveCount(0) fails.

    The gated half holds under the lock for both navigation types regardless of warm state: the soft-nav client lock gates dynamic-data writes, and on an initial load the server honors the cookie on the document request and suspends dynamic data. A vacuous pass is only possible with a build produced WITHOUT the testing API, which defense #1 above covers.

Determinism and the rig

  • Always measure on a production build, never next dev; SKILL.md phase A owns this invariant and its rationale.
  • Run the RED several times; an intermittently red gate is not a gate. If it flakes, determine whether the cause is infrastructure (transient errors) or a real race before trusting either color.
Referenced from SKILL.md
SKILL.mdView in source ↗
Source excerpt starting at line 122.
- [ ] 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
SKILL.mdView in source ↗
Source excerpt starting at line 127.
- [ ] 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)
SKILL.mdView in source ↗
Source excerpt starting at line 186.
   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
SKILL.mdView in source ↗
Source excerpt starting at line 234.
route cannot build while blocked, or a cookie/session read stays GREEN, use theRED recipes in `reference/red-test-robustness.md`.
SKILL.mdView in source ↗
Source excerpt starting at line 245.
untrustworthy REDs, the checklist, and worked cases are in**`reference/red-test-robustness.md`**. Read it now.
SKILL.mdView in source ↗
Source excerpt starting at line 407.
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.
SKILL.mdView in source ↗
Source excerpt starting at line 414.
test-trustworthiness items are the robustness checklist(`reference/red-test-robustness.md`); confirm them, then require thesePR-specific items:
SKILL.mdView in source ↗
Source excerpt starting at line 447.
  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