Next.js 16.3's Instant Navigations model (cacheComponents + partialPrefetching)
makes caching explicit and is slated to become the default in a future major. Migrating
early is cheap; migrating under a forced major upgrade is not. This is the runbook.
When to use
- Upgrading a client app to Next.js 16.x
- Enabling
cacheComponents/ Instant Navigations on an existing App Router app - A navigation-speed complaint on an app still relying on implicit caching
Steps
- Baseline first. Record current behavior for the key routes: what is static, what streams, navigation timings. Surprises during migration get judged against this, not memory.
- Flip the flags on a branch:
cacheComponents: trueandpartialPrefetching: trueinnext.config.ts. Build — the errors are the audit: every route that silently depended on implicit caching now says so. - Make caching explicit route by route: add
'use cache'to work that is cacheable (it now works client-side too); give every dynamic route a Suspense loading shell so navigations paint the static frame instantly while dynamic holes stream in. - Audit the assumptions the errors didn't catch: fetches that assumed automatic dedupe/caching, route handlers assumed static, anything reading cookies/headers inside a would-be-cached scope — move the dynamic read below the Suspense boundary or out of the cached function.
- Lock it with tests: navigation tests asserting the shell paints before data
(Playwright's
instant()helper where available), plus one test per route that must stay dynamic. - Compare against the baseline and ship route group by route group — not the whole app in one merge.
Rules
- Never enable the flags and ship on a green build alone — the failure mode is stale data rendered confidently, which no build error catches.
- Every
'use cache'gets a deliberate revalidation story (time, tag, or on-mutation); "cached forever by accident" is the migration's main bug. - Personalization goes below a Suspense boundary, not into the cached shell — auth-dependent UI in a cached component is a data leak, not a perf win.
- Document per route group: cached / streamed / fully dynamic, and why.
Examples
Good: "Marketing routes: fully cached, revalidate daily. Dashboard: static shell +
streamed org data below Suspense; instant() test pins the shell paint."
Bad: Flags on, build green, shipped — pricing page now shows yesterday's prices
and nothing failed.