Web Frameworks

Migrate to Next.js Cache Components Without Breaking It


— Photo by Markus Spiske on Unsplash
The short answer

Migrating to Next.js Cache Components means enabling the `cacheComponents` flag, replacing `dynamic`, `revalidate`, and `fetchCache` exports with `use cache` and `cacheLife`, and converting routes incrementally with the `instant = false` opt-out and Vercel's official codemod. It requires Next.js 16+. Component state now persists across navigation, and validation errors only appear in the dev overlay, not the build output or HTTP response.

Your Next.js dashboard used to load instantly. Now next dev eats 15GB of RAM by lunch, and every build takes longer than the last one. Next.js 16.3 has a fix, and it isn't just a performance flag.

It's called Cache Components, and if you're about to migrate to Next.js Cache Components on a live client site, the honest answer is: it will break things before it fixes anything. Here's the order that keeps you out of a 2am rollback.

What Cache Components Actually Changes

Cache Components is a single config flag — cacheComponents: true — that replaces three older experimental flags (ppr, dynamicIO, useCache) with one unified model. Data fetching becomes dynamic by default. You decide what gets cached, at the page, component, or function level, using the use cache directive.

Next.js then prerenders a static HTML shell for a route and streams in the dynamic parts once they're ready. That's the whole pitch: pages that render instantly for the parts that can be known ahead of time, and stream the rest.

It shipped as a stable flag in Next.js 16.0. Next.js 16.3 — the release current as of this writing — calls it the biggest update to the framework since 16.0 shipped. The migration guide itself was last updated September 7, 2026. Vercel is still refining the path, which is a sign to move carefully, not skip steps.

One of the fixes below won't show up anywhere in your build log. It only shows up when a client calls asking why a form on their site "looks broken but still works." We'll get to that.

How Do You Migrate to Next.js Cache Components?

Vercel's own migration guide is direct about this: don't convert your whole app in one pass. Do it in order.

  • Enable the flag first. Add cacheComponents: true to next.config.ts. Nothing else changes yet — but every route segment still exporting dynamic, revalidate, or fetchCache will now throw a build error.

  • Opt every route out before converting anything. Run the official codemod — npx @next/codemod@canary cache-components-instant-false ./app — which adds export const instant = false to every page, layout, and default export that doesn't already declare it. Your app keeps building and serving while you work through routes one at a time.

  • Fix synchronous I/O immediately, because it can't be deferred. Calls like new Date(), Math.random(), and crypto.randomUUID() during prerender throw a build error that instant = false does not clear. Move them behind connection() inside a <Suspense> boundary, or into a Client Component.

  • Convert one route at a time. Remove instant = false from a route, then resolve whatever the dev overlay flags — usually by adding use cache with a cacheLife profile, or wrapping runtime data access in <Suspense>.

Vercel also publishes an agent skill for this exact job — next-cache-components-adoption, installed with npx skills add vercel/next.js --skill next-cache-components-adoption. It runs in two modes. Picking the wrong one on a client site you can't take down for an afternoon means redoing the work. More on that below.

There's also a failure mode in here that has nothing to do with caching, code, or your build log at all. It shows up as a support ticket, not a stack trace. We'll get to it once the caching pieces are out of the way.

The Five Things That Break First

These are the route segment configs and APIs that error out the moment you flip the flag, and what replaces each one:

  • dynamic = 'force-dynamic'. Delete it. Every route is dynamic by default now, so the export does nothing.

  • dynamic = 'force-static'. Delete it too, but add use cache with cacheLife('max') wherever the route reads uncached data, or the build will fail on the first fetch call it can't resolve statically.

  • revalidate = <seconds>. Replace it with cacheLife() inside a use cache function — pick the closest built-in profile ('hours', 'days', 'weeks') or define a custom one if your number doesn't line up.

  • unstable_cache(...). Replace it with a plain function marked 'use cache', plus cacheTag() for anything you invalidate with revalidateTag.

  • generateStaticParams returning []. This now throws a build error outright. It has to return at least one real param so Next.js can prerender a non-empty static shell; unlisted paths still render at request time and upgrade afterward.

Route Handlers deserve their own warning. A GET handler that hits uncached or runtime data bails out of prerendering by throwing. If you already wrap handlers in try/catch, that catch block swallows the bail-out silently. The build still succeeds. Nothing looks wrong until a client's data stops updating, and your CI run is green with no clue why. Set experimental.hideLogsAfterAbort: true to quiet the noise once you understand it. Not before.

DevAegis ships your code encrypted so it only runs while the invoice is current. See how it works

Adopting Cache Components Incrementally

This is where the two adoption-skill modes matter. Incremental mode opens one mechanical pull request that opts every route out with instant = false, then ships each feature conversion as its own follow-up PR — the same flow as the manual codemod steps above, just automated. Direct mode converts every route in place on a single branch.

For a client site that has to stay up, and where a half-migrated state might sit in production for two weeks while you get to the rest, incremental is the only sane choice. Direct mode is for a small, greenfield project where nobody's depending on the site not breaking mid-week.

The Surprise Nobody Warns You About

Here's the fix that doesn't show up in a build log. Cache Components changes how Next.js handles client-side navigation. Instead of unmounting a route when you navigate away, it uses React's <Activity> component in hidden mode — the route stays mounted, just invisible.

That means component state survives. Scroll position, form inputs, expanded accordions — all preserved when a user navigates back. Framed as a feature, this is genuinely nice. Framed as a bug report, it looks like this: a client submits a form, sees a success message, clicks away, clicks back — and the success message is still there, because the component never actually unmounted and reset.

Dropdowns and popovers that used to close on navigation now stay open. Dialogs with focus-on-open logic don't refire, because the effect that ran it already ran once and the state never reset. None of this is a caching bug. It's a rendering-lifecycle change that happens to ship in the same release as the caching change, which is exactly why it catches people off guard mid-migration.

The fixes are specific, not general:

  • Close dropdowns and popovers in a useLayoutEffect cleanup function instead of relying on unmount.
  • Derive dialog open/closed state from the URL rather than from an effect that assumed it would rerun.
  • Reset form state and useActionState results explicitly in the submit handler, since returning to the page no longer does it for you.

Is It Safe to Ship This to a Client Site Today?

Cache Components has been stable since 16.0, and the migration guide was refreshed as recently as September 7, 2026 on 16.3.5 — this is an actively maintained path, not an abandoned experiment.

Here's the part worth being honest about. Validation issues from the migration don't appear in the HTTP response. A slow route still returns a normal 200 with working HTML. The warning only shows up in the dev overlay, the dev server log, or the get_errors MCP tool. If nobody watches that output, a route ships half-migrated. Nobody notices until it's slow in front of a client, not in a browser tab.

That argues for the incremental path even on a single-developer project, not just an agency juggling several. Convert a route, watch the overlay clear, ship it, move to the next one. Skipping straight to "flip the flag and see what breaks in production" is how a migration turns into an incident.

Where This Fits If You Ship for More Than One Client

A Cache Components migration produces a new build for every site you touch — new .next output, a new deploy, a new delivery event. That's true whether the migration takes an afternoon or three weeks spread across a dozen client projects.

It's also, quietly, a moment where a protection gap opens up. Some freelancers keep a client's build tied to their payment terms — see how to protect your code as a freelancer. A routine migration like this doesn't feel like "shipping the project." It feels like "upgrading a dependency." That's exactly when the re-protection step gets skipped. DevAegis re-encrypts on every build run through its CLI, so a cache-components upgrade doesn't quietly become the one delivery that went out unprotected.

Juggling this across several client codebases makes it worse. The hard part usually isn't the migration itself. It's keeping track of which deliverables are protected and which aren't once five upgrade PRs are open at once. And a kill switch only works if the client agreed to it up front, in the contract — never as a surprise they were never told about.

FAQ

Does Cache Components require Next.js 16? Yes. The flag was introduced in Next.js 16.0 and requires the App Router on the Node.js runtime — the deprecated edge runtime isn't supported. If you're on Next.js 15 or earlier, work through the version 16 upgrade guide first.

Will migrating break my existing fetch caching? No. Your existing fetch cache and unstable_cache calls keep working as a separate caching layer while you migrate route by route. You don't have to convert everything before any of it works.

Is there a tool that migrates this automatically? Vercel publishes an official coding-agent skill, next-cache-components-adoption, with incremental and direct modes, plus a standalone codemod (cache-components-instant-false) that handles the opt-out step by itself. Neither one replaces reading the dev overlay's validation output route by route.

What breaks the instant I turn on the flag? Any route segment still exporting dynamic, revalidate, or fetchCache throws a build error immediately. Those three need to be replaced with use cache, cacheLife(), and cacheTag() before the build succeeds again.

Does this affect projects using Pages Router instead of App Router? No — Cache Components, use cache, and the associated migration guide apply to the App Router. Pages Router caching is unaffected by this flag.

Key takeaways

  • Cache Components (`cacheComponents: true`) requires Next.js 16 or later and replaces the `dynamic`, `revalidate`, and `fetchCache` route segment configs with `use cache` and `cacheLife`.
  • Vercel's recommended path is incremental: enable the flag, run the `cache-components-instant-false` codemod to opt every route out first, then convert routes one at a time.
  • `generateStaticParams` returning an empty array now throws a build error under Cache Components — it must return at least one real param.
  • Component state (open dropdowns, dialogs, form inputs) now persists across client-side navigation, because Next.js keeps routes mounted in React's hidden Activity mode instead of unmounting them.
  • Validation issues from an incomplete migration only show up in the dev overlay or the `get_errors` MCP tool, not in the HTTP response, so a half-migrated route can ship without an obvious failure.
Sources
  1. Migrating to Cache Components — Next.js (Vercel)
  2. cacheComponents config reference — Next.js (Vercel)
  3. Next.js 16.3 — Next.js (Vercel)

Frequently asked questions

Straight answers to what people ask about migrate to next.js cache components.

Yes. The `cacheComponents` flag was introduced in Next.js 16.0 and requires the App Router running on the Node.js runtime — the deprecated edge runtime isn't supported. Projects on Next.js 15 or earlier need to complete the version 16 upgrade guide first.

Stop handing over the leverage

Your code ships encrypted and runs only while you allow it. One toggle and their site shows a payment screen.

Protect Your Code