Skip to content
Zumkai

Next.js 16: what breaks in the migration

There are 21 breaking changes and the official codemod covers five. The rest takes the build down with no warning; one of them hits anyone using smooth scroll.

  • next.js
  • migration
Card with the number of breaking changes in Next.js 16 and how many the official codemod resolves.
Contents
  1. Before you start: the minimums
  2. What the codemod resolves, and what it does not
  3. Turbopack by default: your build can fail on purpose
  4. Async Request APIs: the grace period ended
  5. middleware became proxy, and edge got left behind
  6. Cache: a new signature and a flag that is no rename
  7. next/image: six defaults changed
  8. What is gone for good
  9. Three tooling changes that slip by
  10. The scroll change almost nobody saw
  11. The path Vercel recommends: migrate with an agent
  12. The order I would follow
  13. Frequently asked questions
  14. Sources

The official codemod resolves five things. The other sixteen you do by hand, and several take the build down without hinting at why.

It is no routine update. Next.js 16 closed the compatibility period version 15 had opened, swapped the default bundler, renamed middleware, changed six next/image defaults and removed APIs that had existed forever.

This is the map of what breaks, in the order you will meet each item.

Before you start: the minimums

Three barriers that stop the install before any code:

RequirementChange
Node.jsMinimum 20.9.0 (LTS). Node 18 is no longer supported
TypeScriptMinimum 5.1.0
BrowsersChrome 111+, Edge 111+, Firefox 111+, Safari 16.4+

If your deploy runs on Node 18, solve that first, because nothing else matters until then. Anyone following the deploy documentation with GitHub and Hostinger has to check the Node version in the panel before pushing.

What the codemod resolves, and what it does not

Start with it:

bash
npx @next/codemod@canary upgrade latest

It covers five things and no more: it updates next.config.js to the new Turbopack configuration, migrates from next lint to the ESLint CLI, renames middleware to proxy, strips the unstable_ prefix from stabilized APIs and removes experimental_ppr from pages and layouts.

The upgrade runs less than every migration. If your app still uses synchronous access to Request APIs, run this one too:

bash
npx @next/codemod@canary next-async-request-api .

And if you used next lint, a third exists:

bash
npx @next/codemod@canary next-lint-to-eslint-cli .

Everything outside those three lists is manual work.

Official codemod coverage in the migration to Next.js 16 The upgrade codemod resolves five changes; the rest demands manual work, including image defaults, removals and behavior changes. WHAT THE `upgrade` CODEMOD COVERS 5 the rest is manual Automatic Turbopack config · next lint → ESLint CLI · middleware → proxy · unstable_ removal · experimental_ppr removal Manual 6 next/image defaults · default.js in parallel routes · revalidateTag signature · edge in proxy · scroll-behavior · AMP · runtime config… Two extra codemods exist: next-async-request-api and next-lint-to-eslint-cli
Source: official upgrade guide for version 16, accessed 20 August 2026.

Turbopack by default: your build can fail on purpose

From 16 onward, Turbopack is stable and used by default in next dev and next build. The --turbopack and --turbo flags became unnecessary.

The problem appears in a project with webpack configuration: the build fails on purpose, to avoid a silent configuration error. Three ways out:

json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build --webpack"
  }
}

Using --webpack keeps the old behavior in the build. As alternatives, next build --turbopack ignores your webpack config, or you migrate the config to the Turbopack equivalent.

One common trap: if the build fails complaining about a webpack config and you wrote none, the odds are a plugin is injecting the option.

Two smaller changes that catch people: the configuration moved from experimental.turbopack to turbopack at the top of nextConfig, and Turbopack supports no Sass tilde (~). @import '~bootstrap/...' becomes @import 'bootstrap/...'.

Async Request APIs: the grace period ended

Version 15 introduced the asynchronous Request APIs with temporary synchronous compatibility. In 16, synchronous access is gone.

It applies to cookies(), headers(), draftMode(), params in layout, page, route, default, opengraph-image, twitter-image, icon and apple-icon, and searchParams in page.

One detail the codemod misses catches anyone generating dynamic images: in the opengraph-image, twitter-image, icon and apple-icon functions, id became a Promise too:

js
// Next.js 16
export default async function Image({ params, id }) {
  const { slug } = await params
  const imageId = await id   // now a Promise<string>
}

The same happened with sitemap's id. And note the asymmetry: generateImageMetadata still receives a synchronous params.

To migrate with types, npx next typegen generates the PageProps, LayoutProps and RouteContext helpers:

tsx
export default async function Page(props: PageProps<'/blog/[slug]'>) {
  const { slug } = await props.params
}

middleware became proxy, and edge got left behind

The middleware file became proxy, to make the network boundary clear. The named export changes too:

ts
// proxy.ts
export function proxy(request: Request) {}

Configuration flags follow: skipMiddlewareUrlNormalize became skipProxyUrlNormalize.

The catch the rename hides: proxy supports no edge runtime. It runs on nodejs, and that is not configurable. Anyone depending on edge has to stay on middleware, and the documentation promises instructions in a minor release.

Meaning: the rename hides an architecture decision, and the codemod will rename your file without asking whether you use edge.

Cache: a new signature and a flag that is no rename

Two independent changes, easy to confuse.

revalidateTag now demands a second argument with the cacheLife profile. The one-argument form now carries a deprecation and produces a TypeScript error:

ts
revalidateTag('posts')          // before
revalidateTag('posts', 'max')   // now

If you need immediate expiry instead of stale-while-revalidate, the new updateTag API exists, exclusive to Server Actions, with read-your-own-writes semantics: the user makes the change and sees the result at once. Calling it outside a Server Action throws, so in a Route Handler and a webhook, use revalidateTag with a profile.

They also stabilized cacheLife and cacheTag. The unstable_ imports can go.

The second change is bigger than it looks. The experimental Partial Prerendering flag is out, along with experimental.dynamicIO, experimental.useCache and the experimental_ppr segment config. The replacement is cacheComponents: true, and the documentation warns that it is no rename:

With the flag on, the dynamic, revalidate and fetchCache route segment configs start throwing, replaced by use cache and cacheLife. And one detail blocks the build with no escape: synchronous IO calls in the prerender (new Date(), Date.now(), Math.random(), crypto.randomUUID()) fail the build and allow no deferral, the instant = false escape option included.

If you use PPR today, the official recommendation is staying on the version 15 canary you already run.

next/image: six defaults changed

None of them breaks the build. All of them change behavior in production.

SettingBeforeNow
minimumCacheTTL60 seconds4 hours (14400s)
imageSizesincluded 1616 removed from the default array
qualitiesall allowed[75] alone
maximumRedirectsunlimited3
Local IPallowedblocked, absent dangerouslyAllowLocalIP
Local query stringfreedemands images.localPatterns.search

The qualities one surprises most: if you pass quality={90}, the value gets coerced to the nearest in the list, meaning 75. Your image ends up at a quality other than the one you asked for, with no error at all.

Beyond that, Next marked next/legacy/image deprecated and images.domains too, with images.remotePatterns as the replacement.

What is gone for good

RemovedReplacement
AMP support (next/amp, useAmp, amp config)None
The next lint commandBiome or ESLint directly. next build runs lint no more
serverRuntimeConfig and publicRuntimeConfigEnvironment variables, with NEXT_PUBLIC_ for the client
devIndicators.appIsrStatus, buildActivity, buildActivityPositionThe indicator itself remains
unstable_rootParamsnext/root-params

Two structural changes in the same package. Parallel routes now demand an explicit default.js in each slot, and the build fails without it. And next build stopped reporting the size and First Load JS metrics, because the team itself considered them imprecise in a Server Components architecture.

Three tooling changes that slip by

ESLint changed format. @next/eslint-plugin-next now uses Flat Config by default, aligned with ESLint v10, which will drop the legacy format. If you are still on .eslintrc, the migration joined the queue.

dev and build run at the same time. Both moved to separate output directories (next dev writes to .next/dev), and a lockfile prevents two instances of the same command in the same project. In practice, leaving the dev server running while validating a build works.

process.argv no longer contains 'dev'. Before, the config file got loaded twice in development: in the command and in the server. Now it loads once, and the consequence is that checking process.argv.includes('dev') inside next.config returns false.

That breaks a plugin triggering a side effect in development. The swap is direct:

js
// Before, and it works no more
const isDev = process.argv.includes('dev')

// Now
const isDev = process.env.NODE_ENV === 'development'

The typegen and build commands stay visible in process.argv. Only dev vanished.

The scroll change almost nobody saw

This one appears on no highlight list and it hits every site with motion.

In earlier versions, if you had scroll-behavior: smooth on <html>, Next.js overrode that during a route transition: it swapped to auto, navigated with an instant jump to the top, and restored the original value. That is what kept navigation between pages feeling instant even with smooth scroll active.

In 16, that override is gone. Next.js respects your scroll-behavior.

The practical effect: anyone with global smooth scroll will see route navigation turn into an animated scroll to the top, instead of the dry jump. On a long page, that is noticeable and it can look like a bug.

To recover the old behavior, the opt-in is an attribute:

tsx
export default function RootLayout({ children }) {
  return (
    <html lang="en" data-scroll-behavior="smooth">
      <body>{children}</body>
    </html>
  )
}

If your site depends on smooth scroll, the catalog of motion techniques for the web covers the alternatives.

Worth checking alongside: if you enable cacheComponents, React's <Activity> starts holding routes in hidden mode instead of unmounting them. That means useState, form values and scroll position stop resetting when you navigate away and back. A dropdown stays open, a dialog skips redoing its focus effect, a form keeps the result of the submit.

The path Vercel recommends: migrate with an agent

The official upgrade guide's first section carries the title "Use an AI agent (recommended)", and it brings a ready prompt to paste.

That is no loose marketing. From 16.3 onward, running next dev with an agent detected in the environment generates AGENTS.md and CLAUDE.md on its own at the project root. The generated CLAUDE.md is one line:

md
@AGENTS.md

And the managed block inside AGENTS.md opens with a warning worth reading:

The framework now bundles its own documentation inside the next package, at node_modules/next/dist/docs/, with a matched version. Updating Next.js updates the docs the agent reads. To turn it off, agentRules: false.

One statement in that guide interests anyone building an agent setup:

It is the first public measurement I have seen on that trade-off, and it runs against the intuition of "move everything into skills and keep the context light". For knowledge the agent needs on every task, Vercel measured that loading it every time wins.

Reading it alongside the anatomy of a CLAUDE.md that works pays off: the two sources arrive at the same place by opposite paths.

Worth knowing too that a built-in MCP server exists at /_next/mcp, exposing routes, logs and compilation problems from the dev server. An official next-dev-loop skill covers the edit-and-check cycle, and it falls in the same category as the ones I gathered in the catalog of evaluated skills.

The order I would follow

  1. Node 20.9 and TypeScript 5.1 in the environment and in CI. Before anything.
  2. Run the three codemods: upgrade, next-async-request-api, next-lint-to-eslint-cli.
  3. Decide on Turbopack before running the build. If you have a webpack config, choose between migrating and --webpack.
  4. Check edge before accepting the proxy rename. It is the least reversible decision on the list.
  5. next build and resolve what shows up. Parallel routes with no default.js appear here.
  6. Check the six next/image changes in production, since none breaks the build and all change the result.
  7. Test navigation with smooth scroll, if you use it.
  8. cacheComponents waits. It is its own migration, no part of this one.

Steps 1 through 5 are the migration. Steps 6 through 8 are what you discover in production if you skip them.

Frequently asked questions

Can I migrate without adopting Cache Components?

You can, and it is the recommendation. cacheComponents is optional in 16 and represents a separate migration, with a cache model of its own. If you used experimental.dynamicIO or experimental.useCache, remove the flags. If you use experimental PPR today, the official guidance is staying on the version 15 canary until you plan the adoption.

Is the codemod safe to run straight on main?

Run it on a branch. It renames files, touches next.config and alters imports, and the middleware to proxy rename is the most delicate case, because it changes the available runtime with no warning.

My build fails complaining about webpack and I use no webpack. Why?

Almost always a plugin injecting the webpack option into the config. The documentation names that case. Either you identify the plugin, or you pass --webpack in the build.

Do I genuinely need an agent to migrate?

No. The guide offers the full manual path. But if you already use Claude Code or Codex, running next dev on 16.3+ only to watch AGENTS.md get generated pays off, since the docs bundled per version solve the problem of the agent answering with version 14's API.

Sources

Verified on 20 August 2026.

Review trigger: revisit when (a) the minor release with the edge runtime instructions for proxy lands, (b) cacheComponents stops being optional, (c) the next/image defaults change again, or (d) the automatic AGENTS.md generation changes behavior.