
Next Cache Components Optimizer
- 5.5k installs
- 141k repo stars
- Updated August 5, 2026
- vercel/next.js
Optimize a Next.js app that has `cacheComponents: true` — either the static shell on first paint, or the in-app navigation between routes. Picks the matching diagnostic loop and runs it.
About
The next cache components optimizer skill Optimize a Next.js app that has `cacheComponents: true` - either the static shell on first paint, or the in-app navigation between routes. Picks the matching diagnostic loop and runs it. Documentation covers workflows, commands, and guardrails agents should follow when users invoke this capability. Key documented areas include **Page-render loop** ([ppr-loop.md](./ppr-loop.md)) - grow the static shell of a single page. Rank Suspense fallback areas on a shell-only render.; **Nav loop** ([instant-nav-loop.md](./instant-nav-loop.md)) - when the user clicks a link from A to B, show B's static layout immediately (chrome, structure, content-shaped fallb; `next-dev-loop` initiated for this session - it opens the headed browser, exposes the `agent-browser` CLI, and wires the dev MCP server that provides `mcp get_logs`.; `cacheComponents: true` in `next.config.ts`. Refuse otherwise. Reference commands include agent-browser cookies set next-instant-navigation-testing '[0,"p<random>"]' \; --url <origin>. Use when developers or agents need structured guidance for next cache components optimizer tasks with evidence grounded in the bundled SKILL.
- **Page-render loop** ([ppr-loop.md](./ppr-loop.md)) - grow the static shell of a single page. Rank Suspense fallback a
- **Nav loop** ([instant-nav-loop.md](./instant-nav-loop.md)) - when the user clicks a link from A to B, show B's static
- `next-dev-loop` initiated for this session - it opens the headed browser, exposes the `agent-browser` CLI, and wires t
- `cacheComponents: true` in `next.config.ts`. Refuse otherwise.
- Confirm `cacheComponents: true`.
Next Cache Components Optimizer by the numbers
- 5,505 all-time installs (skills.sh)
- +992 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #95 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
next-cache-components-optimizer capabilities & compatibility
- Capabilities
- **page render loop** ([ppr loop.md](./ppr loop.m · **nav loop** ([instant nav loop.md](./instant na · `next dev loop` initiated for this session it · `cachecomponents: true` in `next.config.ts`. ref · confirm `cachecomponents: true`.
- Use cases
- planning
npx skills add https://github.com/vercel/next.js --skill next-cache-components-optimizerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5.5k |
|---|---|
| repo stars | ★ 141k |
| Last updated | August 5, 2026 |
| Repository | vercel/next.js ↗ |
How do I handle next cache components optimizer tasks with agent guidance?
Optimize a Next.js app that has `cacheComponents: true` — either the static shell on first paint, or the in-app navigation between routes. Picks the matching diagnostic loop and runs it.
Who is it for?
Teams needing documented next cache components optimizer workflows.
Skip if: Generic advice without reading bundled docs.
When should I use this skill?
Optimize a Next.js app that has `cacheComponents: true` — either the static shell on first paint, or the in-app navigation between routes. Picks the matching diagnostic loop and runs it.
What you get
Structured workflow from next cache components optimizer documentation applied to the user request.
Files
next-cache-components-optimizer
Two loops, shared levers and primitives, different diagnostics:
- Page-render loop (ppr-loop.md) — grow the static shell of a single page. Rank Suspense fallback areas on a shell-only render.
- Nav loop (instant-nav-loop.md) — when the user clicks a link from A to B, show B's static layout immediately (chrome, structure, content-shaped fallbacks) instead of holding A's UI until B's data resolves. Capture B's suspended boundaries post-
pushstate, classify each bysuspended_by[].name, drop SSR-only client hooks.
Pick one and run it end-to-end.
requires
next-dev-loopinitiated for this session — it opens the headed browser, exposes theagent-browserCLI, and wires the dev MCP server that providesmcp get_logs.cacheComponents: trueinnext.config.ts. Refuse otherwise.
preflight (shared)
1. Confirm cacheComponents: true. 2. The user must already be at the page each loop needs in the headed browser (from next-dev-loop) — logged in, with any state set up. This skill can't drive auth, SSO, or MFA; it takes the manual setup as the starting point. (Each sub-loop names which page it expects.) 3. agent-browser get url to anchor the current route.
Each loop sets the instant cookie as needed (see the shared instant cookie section below).
instant cookie (shared)
Both loops use the next-instant-navigation-testing cookie to freeze the framework's dynamic-data writes. Once set, visible content on the page is the static shell + Suspense fallbacks — that's what we capture to assess the optimization.
Set it with a pending-lock tuple [0, "<unique-id>"]. The id is any unique string; the convention is a p-prefixed random stamp so concurrent scopes don't collide:
agent-browser cookies set next-instant-navigation-testing '[0,"p<random>"]' \
--url <origin>Each loop's preflight specifies when to set it within the flow. Clear it at the end (see teardown below).
decide which loop
- Page-render when the complaint is about one route's initial load. Read ppr-loop.md.
- Nav when it's about navigating between two routes. Read instant-nav-loop.md.
Ambiguous → ask.
shared refactor levers
- Push down — extract I/O into a Suspense-wrapped child so the parent stays static and static siblings lift into the shell.
- Recurse, don't blind-wrap. If a Suspense boundary already wraps a component containing both static content and the I/O, read inside, extract the I/O-dependent JSX into a new leaf, and lift the static siblings up.
- Cache —
'use cache'+cacheLife(<profile>). Always ask the user for freshness; map to a preset (seconds/minutes/hours/days/weeks/max/default).
Push-down and cache compose: push-down lifts static structure, cache eliminates the remaining data gap.
propose via plan mode (shared)
Each refactor goes through plan mode before applying. Treat this as a signal: the application work is non-trivial agentic engineering, not a templated edit. This skill provides the framework — which lever to reach for, which candidate to fix, what the expected visible delta is — but the real work (which file to edit, how to cleanly extract the I/O, where to place the new Suspense boundary, which cacheLife profile to ask the user for) is a judgment call you have to think through. Plan mode forces a coherent proposal before touching code, and gives the user a chance to redirect on any of those decisions.
no-shell bailout (shared)
The levers presume a shell exists to grow or cache toward. If the route is fully blocking — HTTP 500 with blocking-route or NEXT_STATIC_GEN_BAILOUT in mcp get_logs, or zero Suspense boundaries on a visibly-rendered page — there's no shell. Surface the structural blocker and stop; the user has to wrap the offending dynamic access in <Suspense> before either loop can help.
verify requires a visible delta (shared)
Each loop captures a baseline screenshot of the shell before applying any change, then re-screenshots after. Report both paths in the final summary so the user can see what changed. The two captures must visibly differ — fallback area shrunk, content promoted to the static surface, target fallback gone or content-shaped. Identical-looking captures mean the refactor didn't land; undo. "Compiles cleanly" is not the bar.
Hide the dev overlay before each screenshot. The Next.js dev overlay (<nextjs-portal> at the document root) renders instant-nav guidance, build errors, and other dev chrome that pollute the before/after comparison. Hide it, screenshot, restore:
agent-browser eval "document.querySelector('nextjs-portal').style.display='none'"
agent-browser screenshot <path>
agent-browser eval "document.querySelector('nextjs-portal').style.display=''"anti-patterns (shared)
Don't replace granular Suspense boundaries with a top-level loading skeleton. A loading.tsx for the whole segment, or a root-level <Suspense fallback={<Skeleton />}> (or worse, fallback={null} that blanks the UI), defeats this skill's optimization — which is to extract real static chrome above each granular boundary and use content-shaped fallbacks per region. A coarse "the page is loading" stand-in bypasses the work entirely.
gotchas (shared)
- Dev doesn't prefetch the way production does, and routes compile on first hit — so after a navigation or reload, the DOM keeps updating for noticeably longer than the eventual production experience. Wait patiently for the DOM to stabilize before capturing the React tree or taking a screenshot — e.g., poll
document.documentElement.innerHTML.lengthuntil it's unchanged across two consecutive reads. A fixed short delay risks sampling mid-render. - Don't try to verify nav prefetch by inspecting dev network traffic — dev doesn't fire prefetch requests at all, so the network tab, manual
router.prefetch()calls, and<Link prefetch={true}>will all look broken regardless of whether your code is correct. The cookie-locked SPA-nav recipe in instant-nav-loop.md underverifyis already the canonical recipe for this — it simulates what production would prerender into the prefetched RSC without requiring prefetch to actually fire. Use it; don't invent a network-tab alternative. - The diagnose pipeline can be flaky — DevTools attachment timing, DOM-settle races, and dev compilation effects can each produce inconsistent captures from one run to the next. When a result feels off (a candidate appears that you don't expect, or one you expect doesn't), re-run the diagnose 2–3 times and cross-check; boundaries that appear consistently are real, one-off appearances are noise.
reference (shared primitives)
agent-browser react suspense add --only-dynamic to filter
--json server-side to actually-
suspended boundaries. Each
entry has jsx_source +
suspended_by[] with raw blocker
names (usePathname, cookies,
fetch, cache, ...); classify by
name for per-loop rules
POST /__nextjs_original-stack-frames body { frames: StackFrame[],
isServer, isEdgeServer,
isAppDirectory }; returns one
result per frame with
file:line:column
mcp get_logs dev MCP tool from
next-dev-loop; surfaces
blocking-route /
NEXT_STATIC_GEN_BAILOUT 500s
cacheLife('<profile>') default | seconds | minutes
| hours | days | weeks | maxPer-loop primitives in instant-nav-loop.md.
teardown (shared)
Delete the cookie by name — overwrite with an expired stamp:
agent-browser cookies set next-instant-navigation-testing x \
--url <origin> --expires 1Never agent-browser cookies clear (no args) — wipes auth.
---
Sibling of next-dev-loop — initiate that first.
instant-nav-loop (sub-reference of next-cache-components-optimizer)
In-app navigation optimization: when the user clicks a link from A to B, show B's static layout immediately — chrome, structure, content-shaped fallbacks — instead of holding A's UI until B's data resolves.
Strictly smaller than page-render — only segments newly mounted on B's path need a shell. The LCA layout stays mounted with its already-resolved data, so even a dynamic-root app can have instant in-app nav.
The hard part is identifying real blockers. After pushstate, capture B's suspended boundaries with agent-browser react suspense --only-dynamic --json. Each boundary carries suspended_by[].name — usePathname / useSearchParams / useRouter (client-hook), cookies / headers / connection (request-api), names containing fetch or cache (server-fetch / cache), etc.
Client-hook blockers don't block instant nav. They suspend only during SSR prerender; on a client nav (pushstate) they resolve instantly from the router store. Diagnose drops them as candidates (see step 4 for the operational set), so the loop doesn't recommend SSR-only refactors that have no effect on click-to-paint.
When the user navigates from A to B, the layouts they share stay mounted; only the segments past the point where the two routes diverge actually load. So B's suspended-boundary capture is naturally focused on the new work — you don't have to filter out the shared parts. With multiple real candidates, fix the one highest in the new-segments tree (closest to where A's and B's paths diverge) first.
Complementary to Next.js's Instant Insights, which checks Suspense _existence_ (structural). This loop checks fallback _quality_ (visual) — closing the <Suspense fallback={null}> loophole.
preflight (in addition to shared)
The starting page is A — the route the user is navigating _from_. The shared preflight has already anchored A (the user is on it, logged in, with state set up). Now anchor B:
1. Ask the user to perform the navigation to B in the headed browser — click the link, button, or whatever leads there. Read agent-browser get url to confirm B's URL. 2. agent-browser pushstate <A> to return the browser to A. The diagnose loop starts from A.
Set the instant cookie (per shared instant cookie section) any time after the browser is on A but before calling pushstate. There's no race — the cookie only needs to be present at the moment of navigation. The cookie doesn't block the navigation; it gates the framework's dynamic-data writes, so B's React tree mounts normally but its dynamic Suspense boundaries stay in fallback until the cookie is cleared. (Setting the cookie before a direct load of A or B would freeze that page at its static shell — that's why we wait until the user is on A first.)
loop
diagnose
1. Set the instant cookie and navigate via `pushstate <B>`. Wait for the DOM to settle. B's tree is now mounted; its dynamic boundaries stay in fallback while the cookie holds.
2. Check B for the no-shell bailout per SKILL.md.
3. Capture B's suspended set. agent-browser react suspense --only-dynamic --json → boundaries with suspended_by[].
4. Filter to real nav candidates. Drop boundaries whose suspended_by entries are all client-hook names (usePathname, useSearchParams, useRouter, useSelectedLayoutSegment(s), useParams) — SSR-only, won't help nav. Keep boundaries with at least one request-api, server-fetch, or cache blocker.
5. Gauge the gap.
- No candidates → ask the user if the nav still feels slow. No → stop and offer to audit other A → B pairs. Yes → step 8 (unwrapped async).
- Candidates present → screenshot the locked B state (hide
nextjs-portalper the shared rule), check rendered area. All sub-viewport → already in good shape; stop.
6. Resolve sources for each remaining candidate via POST /__nextjs_original-stack-frames on suspended_by[].owner_stack (or jsx_source if the stack is empty).
7. Pick the highest candidate. With multiple candidates, fix the one highest in B's new-segments tree — closest to where A's and B's paths diverge. If that candidate wraps the others, recurse: read inside the wrapper to find the I/O that's actually blocking.
8. Fallback: unwrapped async. Reached from step 5 when there are no react suspense candidates but the nav still feels slow. The blocker has no <Suspense> so it doesn't appear in the capture. Direct-load B (full page navigation, not pushstate; cookie still set); mcp get_logs surfaces a blocking-route 500 naming the unwrapped I/O. Filter to sources past the point where A's and B's paths diverge; bailouts in shared layouts above are PPR concerns, not nav.
decide / apply
Apply the shared lever rules from SKILL.md; push-down recipes work at layouts too.
Nav-only third lever: private cache + runtime prefetch. For I/O that reads cookies() / headers() / searchParams, shared 'use cache' won't help — those reads bail to dynamic. Use 'use cache: private' + cacheLife({ stale: N }) on the scope that encloses the request-API read (see scope rule below), plus prefetch = 'allow-runtime' as a route segment config (page or layout export) on the segment that owns the private content. Private-cache results live only in the browser — never stored on the server — so allowing runtime prefetching lets Next.js resolve them at link-visibility time with the user's session; the click commits with cookie-derived data already in place.
Scope rule when cookie-read and data-fetch live in different frames. The directive's semantics are about the cache scope enclosing the request-API call, not "the I/O function" as a label. If a page reads cookies() and passes the value into a separate fetch helper, putting 'use cache: private' on the helper alone leaves the cookie read outside any cache scope and the segment stays dynamic. Either move the directive up to the frame that reads cookies, or move the cookie read down into the helper. Compiles and typechecks either way — only runtime behavior tells you which is correct.
The prefetch flag applies to the segment plus every descendant — put it on the most ancestral segment with runtime-cacheable content (layout-level covers all child pages; page-level covers only that page). Server Component only (not allowed with "use client"); requires cacheComponents: true. Valid values: 'auto' (default) / 'force-disabled' / 'force-static' / 'allow-runtime'. It doesn't make any segment cacheable on its own (each still needs 'use cache: private'), doesn't help cold loads (no <Link> to prefetch from), and doesn't override await connection().
verify
Cookie-locked SPA-nav screenshot is the canonical visible delta — what production would have prefetched. Recipe (run once before applying, once after):
1. Be on A. Set the instant cookie. 2. agent-browser pushstate <B> — a real client navigation. 3. Wait for the DOM to settle. With the cookie set, the framework gates dynamic-data writes (see preflight), so the captured state is the static shell + Suspense fallbacks. 4. Hide nextjs-portal, screenshot, restore (per the shared rule).
Compare the two screenshots per the shared visible-delta rule. The after-shot must visibly differ — more static content promoted, content-shaped fallbacks, or (for the private + runtime-prefetch lever) cookie-derived data resolved.
Identical before/after is two distinct signals, not one. Either the lever didn't apply (code is wrong) or no I/O resolved either time (environment is broken — DB unreachable, stale TLS sockets, etc.). Before iterating on the code shape, check mcp get_logs for socket timeouts or fetch failures in either capture window. If the data path didn't complete in either run, the comparison is inconclusive — fix the environment and re-capture.
Also:
1. Re-run the diagnose capture. Repeat steps 1–4 on the new code. The target candidate should be absent from B's suspended set (the blocker is gone), or its fallback should now be content-shaped. 2. Re-check B for the no-shell bailout.
reference (loop-specific)
agent-browser pushstate <url> client-side navigation (no
HTTP request, no full reload)
'use cache: private' per-session cache; cookies /
headers / searchParams reads ok
prefetch = 'allow-runtime' route-level: permits runtime
prefetching of private-cached
content when <Link> is visible
instant = false layout-level opt-out; escape
hatch, anti-patternppr-loop (sub-reference of next-cache-components-optimizer)
Page-render optimization: grow the static shell of a single cacheComponents page.
Rank candidates by visible pixel area; largest gap first.
preflight (in addition to shared)
The page to optimize is whatever route the user is currently on (per shared preflight). Set the instant cookie (per shared instant cookie section), then reload.
loop
diagnose
1. Check for the no-shell bailout per SKILL.md.
2. List candidates. agent-browser react suspense --only-dynamic --json → each boundary has jsx_source (file:line:col) and suspended_by[].name. Resolve jsx_source (or suspended_by[].owner_stack) via POST /__nextjs_original-stack-frames.
3. Rank by rendered area. Per candidate, take max(fallback rect on shell-only, rendered subtree rect on full). Fallback rect alone misleads when developers used an undersized spinner.
4. Gauge the gap. Same capture as verify — the shell-only render. If the top-ranked candidate is sub-viewport (thin fallback bar, sidebar widget), the shell is already in good shape; surface that and offer to audit other routes for better targets, rather than forcing a marginal refactor.
5. One boundary dominates → that wrapper is the shell. Read inside, enumerate the awaits, recurse with those.
decide / apply
Apply the shared lever rules from SKILL.md.
verify
Re-take the shell-only render and compare against the baseline screenshot. The targeted gap must shrink or vanish; identical captures fail per the shared visible-delta rule. Re-check the no-shell bailout; a botched extract can break the shell.
Related skills
FAQ
What does next cache components optimizer do?
Optimize a Next.js app that has `cacheComponents: true` — either the static shell on first paint, or the in-app navigation between routes. Picks the matching diagnostic loop and runs it.
When should I invoke next cache components optimizer?
Optimize a Next.js app that has `cacheComponents: true` — either the static shell on first paint, or the in-app navigation between routes. Picks the matching diagnostic loop and runs it.
What are key capabilities?
**Page-render loop** ([ppr-loop.md](./ppr-loop.md)) - grow the static shell of a single page. Rank Suspense fallback areas on a shell-only render.