next-cache-components-adoption

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.

Install
npx skills add 'https://github.com/vercel/next.js/tree/canary/skills/next-cache-components-adoption'
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 ↗

references/per-page-decisions.md

references/per-page-decisions.mdBrowse 3 files
View on GitHub
← Back to SKILL.md

per-page decisions: removing instant = false

Read this when a route still blocks after you remove its instant = false and the dev overlay's fix card isn't enough on its own. Each section here covers a judgment call the agent shouldn't make alone.

deciding what to do with a blocking read

Read the full linked page behind the fix card — not only the inline snippet — before editing. The card unblocks the build, but the page covers the details that make the route's navigation actually instant (e.g. where to place a <Suspense> boundary). Don't improvise.

If you're unsure which fix fits, the right call usually depends on what this part of the page is for, which the code doesn't capture. Ask the user about their goal for it rather than guessing. Frame it as a product/UX question: should this content be there instantly on load, or is it fine for it to stream in a moment later? Should everyone see the same thing (cacheable) or is it per-user / per-request? Tie the technical fix to that answer (cache it, wrap it in <Suspense>, or keep it request-time), so they're deciding the experience, not the API.

security gates and other code you can't infer

If the blocking code looks like it's there for a reason you can't infer — a security gate at the page top (await verifyAccess(), an auth redirect, a feature-flag check) where moving it inside <Suspense> would change what the code guarantees — stop and ask the user before refactoring. The build error wants <Suspense>, but wrapping a gate in <Suspense> defeats the gate. Only the person who wrote it knows whether to keep the route blocking (instant = false as a documented Block), restructure the page so the gate runs differently, move the check to Proxy, or — if the gate duplicates protection the app already relies on elsewhere (platform auth such as Vercel Deployment Protection, a Proxy check, a Data Access Layer) — remove it.

Relocating a read (await connection(), Proxy) only changes where it runs, never whether it should run at all — so a gate that's redundant or broken looks identical to a correctly-placed one through the fix card's lens. If a gate looks strange, redundant, or out of place, say so plainly — "this looks like it might be unnecessary here — are you sure it belongs?" — instead of quietly relocating it. Surfacing the doubt is the agent's job; deciding is the user's.

If every route under a layout is gated this way, a documented Block on the layout is the correct end state. Moving the gate to Proxy is the architectural fix, not a Cache Components one, and that's a follow-up rather than something to hold the migration on.

For the broader picture, read the Authentication guide (where auth checks belong: Proxy for routing, Data Access Layer for data) and the Data Access Layer section of Data Security (centralized auth checks that compose with 'use cache').

If you don't know how to make a piece of code Cache Components–correct without changing what it does, ask.

when to leave a Block in place

If a route is genuinely meant to block — it's inherently per-request with no useful static shell — or the refactor would be large and the user would rather not take it on now, that's a legitimate outcome. Keep instant = false, but confirm it with the user first and turn its // TODO: Cache Components adoption comment into a reason, e.g. // instant = false: kept on purpose — fully request-time dashboard or // instant = false: deferred, refactor too large for now.

A documented, deliberate Block is fine to leave after the migration; an undocumented leftover opt-out is not.

Referenced from SKILL.md
SKILL.mdView in source ↗
Source excerpt starting at line 208.
- 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).
SKILL.mdView in source ↗
Source excerpt starting at line 219.
- `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 io()` or `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.