Update skills for Partial Prefetching (#96299)

## Summary

- update the Partial Prefetching adoption skill for the current runtime
prefetching model
- scope the Cache Components optimizer's runtime-prefetch limitation to
URL data
- preserve the optimizer pattern structure for URL data that cannot
move, with Partial Prefetching, targeted `<Link prefetch={true}>`, and
cached URL-data content as the requirements
- make the optimizer follow-up conditional: use targeted
`prefetch={true}` when the app already adopted Partial Prefetching,
otherwise recommend the PPF adoption skill for the broader shared App
Shell model

## Why

Andrew's Partial Prefetching work removes the old `allow-runtime`
route-config model. The skills should teach the current shape: Partial
Prefetching gives links a shared App Shell baseline with lower
duplicated prefetch cost, while `<Link prefetch={true}>` is reserved for
links where resolving URL-specific content before click is worth the
extra server work.

## Validation

- `git diff --check HEAD~1..HEAD`
- grep confirmed no `allow-runtime` / `force-runtime` references remain
in the changed Skill files
- attempted `skill-creator` quick validation, but the local Python env
is missing `yaml`
This commit is contained in:
Aurora Scharff
2026-07-29 11:56:31 -04:00
committed by GitHub
parent e73a25825b
commit 91c6309c52
8 changed files with 82 additions and 67 deletions
@@ -65,11 +65,11 @@ After enabling the flag, every `<Link>` prefetches its destination's App Shell,
## What changes for `<Link>`
| `<Link>` prop | Before (Cache Components default) | After Partial Prefetching |
| ----------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `<Link href="/x">` | Prefetched the cached page render. | Loads the shared App Shell for `/x`. |
| `<Link href="/x" prefetch>` | Prefetched the cached page render **and** any dynamic content. | Loads the App Shell. If `/x` opts into [runtime prefetching](/docs/app/guides/runtime-prefetching), also prefetches per-link runtime data. |
| `<Link href="/x" prefetch={false}>` | Disabled prefetching for this link. | Unchanged. Still disabled. |
| `<Link>` prop | Before (Cache Components default) | After Partial Prefetching |
| ----------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `<Link href="/x">` | Prefetched the cached page render. | Loads the shared App Shell for `/x`. |
| `<Link href="/x" prefetch>` | Prefetched the cached page render **and** any dynamic content. | Loads the App Shell, plus per-link runtime data through [runtime prefetching](/docs/app/guides/runtime-prefetching) when `/x` reads it. |
| `<Link href="/x" prefetch={false}>` | Disabled prefetching for this link. | Unchanged. Still disabled. |
The App Shell is shared across every link to a given route, regardless of dynamic params, so rendering many `<Link>`s to the same destination doesn't multiply the work.
@@ -43,7 +43,7 @@ It also assumes your route is already structured for instant navigation. If it i
## What runtime prefetching does
Runtime prefetching is opted into per link with `<Link prefetch={true}>`. Any destination with [Partial Prefetching](/docs/app/api-reference/config/next-config-js/partialPrefetching) enabled supports it — globally via `partialPrefetching`, or per segment with [`prefetch = 'partial'`](/docs/app/api-reference/file-conventions/route-segment-config/prefetch#partial).
Runtime prefetching is opted into per link with `<Link prefetch={true}>`. Any destination with [Partial Prefetching](/docs/app/api-reference/config/next-config-js/partialPrefetching) enabled supports it, either globally via `partialPrefetching` or per segment with [`prefetch = 'partial'`](/docs/app/api-reference/file-conventions/route-segment-config/prefetch#partial).
A user on `/` sees links to `/search?q=react` and `/search?q=next`, each opting in with `prefetch={true}`:
@@ -103,7 +103,7 @@ The prerender advances through anything static or cached, then stops at uncached
More of the page is rendered before the user clicks, with fewer loading states.
Generating it costs **a server invocation per prefetchable link**, so it is opt-in per link. On pages where all the content is statically renderable, Next.js serves the prefetch from the static cache instead; a page that accesses non-static data is prefetched at runtime.
Generating it costs **a server invocation per prefetchable link**, so it is opt-in per link. On pages where all the content is statically renderable, Next.js serves the prefetch from the static cache instead. A page that accesses non-static data is prefetched at runtime.
> **Good to know:** A cold cache (first visit, or after expiration) means the server still has to compute the cached result. Users may see a loading spinner on that first navigation. Subsequent navigations are instant as long as the cache is warm.
@@ -111,7 +111,7 @@ Like `searchParams`, `params` needs a `<Suspense>` boundary, even when the value
## Session data resolves in the shell
Runtime prefetching is for URL data. Session data is handled separately: a route that reads `cookies()` or `headers()`, including through `"use cache: private"`, gets an App Shell that includes its session data, cached per session on the client and ready on navigation without a per-link runtime prefetch.
Runtime prefetching is for URL data. Session data is handled separately. A route that reads `cookies()` or `headers()`, including through `"use cache: private"`, gets an App Shell that includes its session data, cached per session on the client and ready on navigation without a per-link runtime prefetch.
A lookup based on session data needs a cache lifetime, the same way `search(q)` did for the URL. Take a dashboard nav that reads a cookie, then looks up content based on it:
@@ -207,7 +207,7 @@ Skip it when the prefetch can't produce a better UI than the App Shell. Each vis
A per-link runtime prefetch is best-effort. It only helps the navigations where it completes before the click. On a slow connection, on a feed of many links, or on a direct visit, it may not be ready when the user navigates, and the navigation falls back to the App Shell. That shell is the reliable baseline, and runtime prefetching only layers on top when it arrives in time.
When many links to a route are visible at once, such as a grid of cards, each `<Link prefetch={true}>` prefetches that link's content as it enters the viewport, so the grid makes one such server request per card. Prefetch on intent instead: a [hover-triggered prefetch](/docs/app/guides/prefetching#hover-triggered-prefetch) fetches only the links the user is likely to click. The default `<Link>` (without `prefetch={true}`) prefetches just the App Shell, so it doesn't carry this cost.
When many links to a route are visible at once, such as a grid of cards, each `<Link prefetch={true}>` prefetches that link's content as it enters the viewport, so the grid makes one such server request per card. Prefetch on intent instead. A [hover-triggered prefetch](/docs/app/guides/prefetching#hover-triggered-prefetch) fetches only the links the user is likely to click. The default `<Link>` (without `prefetch={true}`) prefetches only the App Shell, so it doesn't carry this cost.
| | App Shell | Per-link runtime prefetch with `prefetch={true}` |
| ------- | ------------------------------------------- | ------------------------------------------------ |
@@ -300,7 +300,7 @@ Prefetching happens when a `<Link />` component enters the user's viewport (init
The following values can be passed to the `prefetch` prop:
- **`"auto"` or `null` (default)**: Prefetch behavior depends on whether the route is static or dynamic. For static routes, the full route will be prefetched (including all its data). For dynamic routes, the partial route down to the nearest segment with a [`loading.js`](/docs/app/api-reference/file-conventions/loading#instant-loading-states) boundary will be prefetched.
- **`true`**: The full route is prefetched for both static and dynamic routes. With [Partial Prefetching](/docs/app/guides/adopting-partial-prefetching) enabled, the prefetch is the [App Shell](/docs/app/glossary#app-shell) plus the per-link runtime data and the cached content behind it — see [Runtime prefetching](/docs/app/guides/runtime-prefetching).
- **`true`**: The full route is prefetched for both static and dynamic routes. With [Partial Prefetching](/docs/app/guides/adopting-partial-prefetching) enabled, the prefetch is the [App Shell](/docs/app/glossary#app-shell) plus the per-link runtime data and the cached content behind it. See [Runtime prefetching](/docs/app/guides/runtime-prefetching).
- `false`: Prefetching will never happen both on entering the viewport and on hover.
> **With Partial Prefetching enabled** ([`partialPrefetching: true`](/docs/app/api-reference/config/next-config-js/partialPrefetching)): the default changes. `auto` prefetches the per-route [App Shell](/docs/app/glossary#app-shell) (the route's static and cached content) instead of the full page. See [Adopting Partial Prefetching](/docs/app/guides/adopting-partial-prefetching) for the full behavior change.
@@ -39,7 +39,7 @@ export default function Page() {
Opts the segment into [Partial Prefetching](/docs/app/guides/adopting-partial-prefetching) without enabling the global [`partialPrefetching`](/docs/app/api-reference/config/next-config-js/partialPrefetching) flag. A `<Link>` pointing at a segment with `prefetch = 'partial'` loads the per-route [App Shell](/docs/app/glossary#app-shell) instead of the legacy full prefetch. Set this on the destination, not the link.
For links that opt into a wider prefetch with [`<Link prefetch={true}>`](/docs/app/api-reference/components/link#prefetch), Next.js may also prefetch the segment at runtime: the server renders a fresh response that resolves per-link runtime data — `params`, `searchParams`, and the full URL. On pages where all the content is statically renderable, Next.js serves prefetches from the static cache; if a page accesses non-static data, it's prefetched at runtime.
For links that opt into a wider prefetch with [`<Link prefetch={true}>`](/docs/app/api-reference/components/link#prefetch), Next.js may also prefetch the segment at runtime. The server renders a fresh response that resolves per-link runtime data (`params`, `searchParams`, and the full URL). On pages where all the content is statically renderable, Next.js serves prefetches from the static cache. If a page accesses non-static data, it's prefetched at runtime.
Use this for incremental adoption when you can't enable `partialPrefetching` for the entire app at once. Once every route in scope has `prefetch = 'partial'`, enable the global flag and remove the per-route exports.
@@ -52,7 +52,7 @@ The pattern is similar to per-route code splitting in single-page apps: one arti
> **Good to know**: Routes that read `cookies()` or `headers()` produce an App Shell that includes session data. The framework auto-detects this and caches the shell per session on the client.
A link can ask for more than the App Shell with [`<Link prefetch={true}>`](/docs/app/api-reference/components/link#prefetch): the prefetch also resolves per-link runtime data like `params`, `searchParams`, and the full URL. See [Runtime prefetching](/docs/app/guides/runtime-prefetching).
A link can ask for more than the App Shell with [`<Link prefetch={true}>`](/docs/app/api-reference/components/link#prefetch). The prefetch also resolves per-link runtime data like `params`, `searchParams`, and the full URL. See [Runtime prefetching](/docs/app/guides/runtime-prefetching).
> **Good to know**: If you use `<Link prefetch={true}>` to a route that hasn't opted into Partial Prefetching, a dev console error suggests enabling `partialPrefetching` app-wide or `prefetch = 'partial'` on the segment. The [dev warning Insight](/docs/messages/instant-link-prefetch-partial) covers each fix in detail.
+37 -25
View File
@@ -359,31 +359,14 @@ this gate is as machine-checkable as the others. Detail:
> 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 the read can't be pushed down** (an ID minted per request, an
all-dynamic page, a per-request auth/scope read the whole subtree needs), there
is no shell to grow. Don't force one: opt the route into **runtime prefetching**
so the prefetch runs the dynamic render ahead of the click and the soft nav
commits the real content. See [Runtime Prefetching](https://nextjs.org/docs/app/guides/runtime-prefetching)
for the mechanism (`prefetch = 'allow-runtime'` on the route plus a full
`<Link prefetch={true}>`) and the [dynamic-data-during-prefetching insight](https://nextjs.org/docs/messages/instant-link-prefetch-partial)
for adoption. The `instant()`-specific gotchas the docs don't cover:
- **The full prefetch is mandatory.** An auto/PPR prefetch bails before the
runtime spawn (`subtreeHasSpeculativePrefetch`); only `prefetch={true}` /
`kind: 'full'` reaches it. If you set `prefetch = 'allow-runtime'` and it's
still RED, the link is doing an auto prefetch.
- **All leaf slots must agree.** `allow-runtime` on the content segment but
nothing on a sibling `@header`/`@sidebar` leaf leaves the route's runtime entry
incomplete, so the lock falls back to the shell. Flip every leaf together.
- **Prefetch the canonical URL.** A link whose href 307-redirects can't be
prefetched — the prefetch receives the redirect, not the tree. Point the link
and the prefetch at the final URL.
- **Don't blanket the full prefetch.** It fetches _all_ the target's dynamic
data; issuing it on hover for every link is wasteful. Scope `kind: 'full'` to
the runtime-prefetch targets only.
- **Marker must be a committed node, not RSC bytes.** The content is often a
client component, so its text isn't in the prefetch response. Assert a
`data-testid` that renders when the client subtree commits.
**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. Runtime 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
[Runtime Prefetching](https://nextjs.org/docs/app/guides/runtime-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
@@ -451,3 +434,32 @@ three hold, you are not done.
- `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 runtime prefetching only where URL-specific content is worth the
extra server work.
@@ -319,36 +319,39 @@ Prefer per-component boundaries inside the page (patterns #1–#5) over one big
**Insight:** the read's own insight surfaces on the client navigation when the boundary is too high — see [where to place the boundary](https://nextjs.org/docs/messages/blocking-prerender-dynamic#choosing-where-to-place-the-boundary).
## 10. Can't push the read down? Runtime-prefetch the whole route
## 10. URL data that can't move
Patterns 1–9 grow a **static shell** by moving dynamic reads behind boundaries. Some routes resist that: an ID minted per request (`createId()`), an auth/scope resolution the whole subtree needs, a page that is _all_ dynamic by nature. The read can't move, so there is no meaningful shell to commit and the soft nav stays RED. The escape hatch is **runtime prefetching** — don't prerender a shell, run the dynamic render _in the prefetch_ so the whole route is warm before the click and the soft nav commits the real content instantly.
Patterns 1–9 grow a **static shell** by moving dynamic reads behind boundaries. Session data from `cookies()` and `headers()` is handled by the earlier patterns. URL data is different: `params`, `searchParams`, and the full URL belong to one link, while the App Shell is shared by every link to the route.
It has **two halves — both required**, and route config alone is RED:
If the whole route depends on URL data, pushing the read lower may leave no meaningful shared shell to commit. That is the optimizer's stop point, not another shell refactor. Return to `SKILL.md` after the optimization loop for the optional runtime-prefetch follow-up.
Runtime prefetching is the only way for this soft navigation to commit the
URL-specific content before the click. It has **three requirements**:
```tsx
// 1. The route opts in — on EVERY leaf (page/default) segment, parallel @slots
// included, or instant validation re-triggers on the segments that lack it
// (the same all-or-nothing coupling as an `instant = false` opt-out).
export const prefetch = 'allow-runtime'
// 1. The destination has adopted Partial Prefetching, either app-wide with
// partialPrefetching: true or route-by-route with prefetch = 'partial'.
// 2. The <Link> asks for a FULL prefetch — that is what spawns the runtime
// request. A default/auto prefetch only warms the static shell.
<Link href={href} prefetch={true}>…</Link>
// <Link prefetch> already issues the full prefetch on hover and on viewport
// entry, so prefer it. An imperative full prefetch via router.prefetch needs
// the non-exported PrefetchKind enum, so it has no clean public form.
// 2. The navigation asks for a full prefetch — normally <Link prefetch={true}>.
// A default/auto prefetch only warms the static shell.
<Link href={href} prefetch={true}>
…
</Link>
// 3. The URL-dependent content is behind `use cache`, keyed by the resolved
// params/searchParams/full URL value.
```
Under `instant()` the runtime entry is what commits, so the real content (not a skeleton) shows under the lock — that is the GREEN.
Under `instant()` the runtime entry is what commits, so the real content, not a skeleton, shows under the lock.
Gotchas (each cost real debugging time):
- **The full prefetch is mandatory.** With App Shells enabled an auto/PPR prefetch bails before the runtime spawn (`subtreeHasSpeculativePrefetch`); only `prefetch={true}` / `kind: 'full'` reaches it. If you set `prefetch = 'allow-runtime'` and it's still RED, the link is doing an auto prefetch.
- **All leaf slots must agree.** `allow-runtime` on the content segment but `instant = false` (or nothing) on a sibling `@header`/`@sidebar` leaf leaves the route's runtime entry incomplete, so the lock falls back to the shell. Flip every leaf together.
- **The full prefetch is mandatory.** With App Shells enabled an auto/PPR prefetch bails before the runtime spawn (`subtreeHasSpeculativePrefetch`); use `<Link prefetch={true}>` for normal links, or keep an existing manual full-prefetch abstraction if the app already owns one. If the route is still RED after caching the URL-dependent content, the navigation may still be doing an auto prefetch.
- **Partial Prefetching must be adopted for the destination.** Runtime prefetching rides on the Partial Prefetching path. If the route is still RED after caching the URL-dependent content, check whether the link is still doing an auto prefetch or whether the destination never adopted Partial Prefetching.
- **Prefetch the canonical URL.** A link whose href 307-redirects (a `/foo` that canonicalizes to `/`) can't be prefetched — the prefetch receives the redirect, not the tree. Point the link and the prefetch at the final URL.
- **Don't blanket the full prefetch.** It fetches _all_ the target's dynamic data; issuing it on hover for every link (recents that point at whole chats) is wasteful. Scope `kind: 'full'` to the runtime-prefetch targets only.
- **Don't blanket the full prefetch.** It fetches _all_ the target's dynamic data; enabling it for every visible link is wasteful. Scope `prefetch={true}` to the runtime-prefetch targets only, using the [runtime prefetching trade-offs](https://nextjs.org/docs/app/guides/runtime-prefetching#per-link-prefetching-trade-offs) and [hover-triggered prefetch](https://nextjs.org/docs/app/guides/prefetching#hover-triggered-prefetch) when many links are visible.
- **Marker must be a committed node, not RSC bytes.** The content is often a client component, so its text isn't in the prefetch response — assert a `data-testid` that renders when the client subtree commits, not a substring of the stream.
Prefer a static shell (patterns 1–9) whenever the read can move: it's cheaper than a runtime prefetch and also covers hard load. Reach for runtime prefetching when the read genuinely can't move, or for a route that's all-dynamic by design.
Prefer a static shell (patterns 1–9) whenever the URL-data read can move: it's cheaper than a runtime prefetch and also covers hard load. Runtime prefetching is only for URL-data reads that genuinely can't move, or routes whose useful content is all URL-specific.
**Insight:** [dynamic data during prefetching](https://nextjs.org/docs/messages/instant-link-prefetch-partial).
@@ -35,9 +35,9 @@ Talk to the user in terms of what they'll see — PRs, features, and how the app
## background
Adopting Partial Prefetching means every route still delivers what its links prefetched before, now split between the App Shell (static and cached content) and the per-link runtime data behind `prefetch = 'allow-runtime'`. 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.
Adopting Partial Prefetching means every route still delivers what its links prefetched before, 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: `partial` warms only the shared App Shell, so a route keyed by `params`/`searchParams` needs [`prefetch = 'allow-runtime'`](https://nextjs.org/docs/app/guides/runtime-prefetching) to prefetch its content, not `partial` (the guide's [URL data](https://nextjs.org/docs/app/guides/adopting-partial-prefetching#url-data) section).
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
@@ -56,15 +56,15 @@ If `partialPrefetching: true` is already set in `next.config.ts`, the app is ado
The work is identical either way — only the commit boundaries differ. 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 the links across the whole source tree, not only `app/` — they often live in `src/components` or shared UI packages: `grep -rnE '\bprefetch\b' --include='*.tsx' --include='*.jsx' .`. Keep the `prefetch={true}` and bare-prop matches (a bare prop is `true`) as the over-prefetching links this audit adopts destinations for, and drop `prefetch={false}` and other values. The same grep also surfaces imperative prefetching: a `router.prefetch(href, { kind: 'full' })` call (often behind a custom hook) is the imperative equivalent of `prefetch={true}`. Audit each such call site's destination the same way you would a link, and verify its prefetch payload in a production build ([step 4](#step-4-verify)) — the flag can change what an imperative `kind: 'full'` prefetch returns, and no insight covers it. If nothing matches, check for a custom link wrapper before calling the audit empty. If there's still nothing, say so in your report and move on to [step 2](#step-2-enable-the-flag).
Enumerate the prefetch sites across the whole source tree, not only `app/` — they often live in `src/components` or shared UI packages: `rg -n '\bprefetch\b|router\.prefetch' -g '*.tsx' -g '*.jsx' .`. Keep the `<Link prefetch={true}>` and bare-prop matches (a bare prop is `true`) as the over-prefetching links this audit adopts destinations for, and drop `prefetch={false}` and other values. Also audit existing imperative [`router.prefetch()`](https://nextjs.org/docs/app/api-reference/functions/use-router#userouter) call sites with the same table, because they can be preserving the same "fetch before navigation" behavior and have no dev insight. For new navigation prefetching, prefer [`<Link>`](https://nextjs.org/docs/app/api-reference/components/link), which the docs call the primary navigation API; use [`router.prefetch()`](https://nextjs.org/docs/app/guides/prefetching#manual-prefetch) only for manual prefetching. If the app already passes an internal `kind` option, treat that as existing implementation detail, not a pattern to spread. If nothing matches, check for a custom link wrapper before calling the audit empty. If there's still nothing, say so in your report and move on to [step 2](#step-2-enable-the-flag).
Then, for each one:
1. **Click it** in `next dev`. The insight fires at navigation time, not when the link prefetches, so a link sitting in the viewport won't trip it — you have to navigate through it. This click is _verification_: it confirms the insight fires before you adopt and clears after. Without a browser, skip the click and adopt from [`link-prefetch-partial`](https://nextjs.org/docs/messages/instant-link-prefetch-partial) and the audit table below — the destination's structure tells you the row, and type-check gates the edit — then leave the live confirmation for the hand-off.
2. **Adopt the destination.** Add `export const prefetch = 'partial'`. That clears the insight for every link pointing at it. If the route reads URL data (`params`, `searchParams`), `partial` warms only its skeleton (the guide's [URL data](https://nextjs.org/docs/app/guides/adopting-partial-prefetching#url-data) section), so it's an `allow-runtime` candidate for step 5, not a finished adoption. Keep `prefetch={true}` on its links and mark the route:
1. **Click each `<Link>` in `next dev`.** The insight fires at navigation time, not when the link prefetches, so a link sitting in the viewport won't trip it — you have to navigate through it. This click is _verification_: it confirms the insight fires before you adopt and clears after. Imperative `router.prefetch()` sites have no equivalent insight, so audit them from source and verify them in production ([step 4](#step-4-verify)). Without a browser, skip the click and adopt from [`link-prefetch-partial`](https://nextjs.org/docs/messages/instant-link-prefetch-partial) and the audit table below — the destination's structure tells you the row, and type-check gates the edit — then leave the live confirmation for the hand-off.
2. **Adopt the destination.** Add `export const prefetch = 'partial'`. That clears the insight for every link pointing at it. If the route reads URL data (`params`, `searchParams`), the default link still warms only its skeleton (the guide's [URL data](https://nextjs.org/docs/app/guides/adopting-partial-prefetching#url-data) section), so it's a runtime-prefetch candidate for step 5, not a finished adoption. Keep `prefetch={true}` on its links and mark the route:
```tsx
// TODO(runtime-prefetch): assess with the user (prefetch = 'allow-runtime')
// TODO(runtime-prefetch): assess with the user whether URL data should resolve before click.
export const prefetch = 'partial'
```
@@ -79,7 +79,7 @@ Then, for each one:
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 only `export const prefetch = 'partial'` and leaves any other value (a deliberate `prefetch = 'allow-runtime'`) in place, along with your `TODO(runtime-prefetch)` markers, which wait for step 5.
2. **Strip the redundant `prefetch = 'partial'` exports.** Run the first-party `remove-partial-prefetch` codemod rather than a text find-and-replace. It removes only `export const prefetch = 'partial'` and leaves other values such as `prefetch = 'force-disabled'` in place, along with your `TODO(runtime-prefetch)` markers, which wait for step 5.
Use the `@canary` channel, not `@latest`. The `remove-partial-prefetch` transform isn't in the stable `@next/codemod` release yet, and `@next/codemod@latest` errors with `Invalid transform choice`.
@@ -87,7 +87,7 @@ Once every audited destination has `prefetch = 'partial'`, finish in two moves.
npx @next/codemod@canary remove-partial-prefetch ./app
```
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'` from every `app/**/{page,layout}.{js,jsx,ts,tsx}` — leave any other `prefetch` value in place, and leave the `TODO(runtime-prefetch)` markers where they are. Don't hand-edit when the codemod can run.
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'` from every `app/**/{page,layout}.{js,jsx,ts,tsx}` — leave other `prefetch` values in place, and leave the `TODO(runtime-prefetch)` markers where they are. Don't hand-edit when the codemod can run.
## step 3: sweep for URL-data insights (after enabling)
@@ -97,7 +97,7 @@ Sweep feature by feature. A feature is a single product surface — `app/setting
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) and opt the route into `prefetch = 'partial'`, 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 outside `<Suspense>` ties the shared shell to one URL. It can surface even inside an existing `<Suspense>` when the boundary sits above the read. Open its docs page and follow the fix there.
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.
@@ -107,10 +107,10 @@ These fixes rarely involve the user — each insight names the offending read an
Checklist before checking in with the user:
- **An empty sweep is the expected outcome when Cache Components adoption finished cleanly** — the prereq already forced every `params`/`searchParams`/`cookies()` read behind `<Suspense>` (surfaced there as `blocking-prerender-*` errors), so a quiet log is success, not a missing signal. Any entry still in the Insights tab is a deliberate, documented decision. To confirm the signal can still fire, check `partialPrefetching` is on, the version is 16.3 or later, and the dev server was restarted after the config change — or move one URL read back outside `<Suspense>`, watch validation fire, then revert. Expect the probe to surface the Cache Components `blocking-prerender-runtime` error rather than the URL-data insight (the upstream check catches the read first) — either one proves the pipeline is alive.
- **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 (prefetching is prod-only) that navigating a changed link lands on the shared shell instantly.
- **If the app prefetches imperatively** (`router.prefetch(href, { kind: 'full' })`), the insight sweep does not cover it, so an empty sweep is not proof the prefetch survived the flag. On the production run, confirm the prefetch payload for a changed link still carries the route's data, not only the App Shell — compare its `_rsc` prefetch response against a full render of the same route (`PerformanceResourceTiming.decodedBodySize`, or diff the RSC payloads). A shell-only payload means the flag changed what `kind: 'full'` returns; restore the data with `use cache` so it rides the App Shell, or `prefetch = 'allow-runtime'` ([step 5](#step-5-runtime-prefetching-optional)).
- **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 runtime-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.
@@ -127,7 +127,7 @@ Then check in with the user. Speak their language — no insight slugs or step l
The audit marked the candidates instead of deciding them. Grep for `TODO(runtime-prefetch)` and walk the list with the user in one conversation. The question per route is whether they want the URL-dependent content prefetched ahead of the click, or streaming in after navigation is fine. A runtime prefetch costs a server invocation per prefetchable link — the guide's [per-link prefetching trade-offs](https://nextjs.org/docs/app/guides/runtime-prefetching#per-link-prefetching-trade-offs) section is the checklist. Don't make these calls alone.
Where the answer is yes, follow the [runtime prefetching guide](https://nextjs.org/docs/app/guides/runtime-prefetching) — add `export const prefetch = 'allow-runtime'` to the route (the codemod in step 2 already stripped the `'partial'` export) and cache the content behind the read using the guide's patterns (`use cache` with the runtime value passed in, or `use cache: private` for per-user data). Each per-link prefetch is a server render, so the [runtime prefetching guide](https://nextjs.org/docs/app/guides/runtime-prefetching#per-link-prefetching-trade-offs) covers the trade-offs, including when to bound it to hover instead of the viewport. Where it's no, delete the marker and leave the route on the default. Either way no `TODO(runtime-prefetch)` marker survives this step. Confirm the opted-in routes against a production run (`next build` and `next start` — the runtime prefetch fires there, not in `next dev`), give the user the same click-through for them, and keep this as its own commit or PR.
Where the answer is yes, follow the [runtime prefetching guide](https://nextjs.org/docs/app/guides/runtime-prefetching): keep [`<Link prefetch={true}>`](https://nextjs.org/docs/app/api-reference/components/link#prefetch) on the links that should resolve more than the App Shell, and cache the content behind the URL-data read using the guide's patterns (`use cache` with the runtime value passed in, or `use cache: private` for per-user data). Each per-link prefetch is a server render when the destination needs non-static data, so use the guide's [per-link trade-offs](https://nextjs.org/docs/app/guides/runtime-prefetching#per-link-prefetching-trade-offs) to decide when viewport prefetching is worth it and when [hover-triggered prefetch](https://nextjs.org/docs/app/guides/prefetching#hover-triggered-prefetch) is a better fit. Where it's no, delete the marker and leave the route on the App Shell default. Either way no `TODO(runtime-prefetch)` marker survives this step. Confirm the opted-in links against a production run (`next build` and `next start` — the runtime prefetch fires there, not in `next dev`), give the user the same click-through for them, and keep this as its own commit or PR.
## further reading