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

Contents
- Before you start: the minimums
- What the codemod resolves, and what it does not
- Turbopack by default: your build can fail on purpose
- Async Request APIs: the grace period ended
- middleware became proxy, and edge got left behind
- Cache: a new signature and a flag that is no rename
- next/image: six defaults changed
- What is gone for good
- Three tooling changes that slip by
- The scroll change almost nobody saw
- The path Vercel recommends: migrate with an agent
- The order I would follow
- Frequently asked questions
- 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:
| Requirement | Change |
|---|---|
| Node.js | Minimum 20.9.0 (LTS). Node 18 is no longer supported |
| TypeScript | Minimum 5.1.0 |
| Browsers | Chrome 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:
npx @next/codemod@canary upgrade latestIt 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:
npx @next/codemod@canary next-async-request-api .And if you used next lint, a third exists:
npx @next/codemod@canary next-lint-to-eslint-cli .Everything outside those three lists is manual work.
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:
{
"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:
// 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:
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:
// 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:
revalidateTag('posts') // before
revalidateTag('posts', 'max') // nowIf 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.
| Setting | Before | Now |
|---|---|---|
minimumCacheTTL | 60 seconds | 4 hours (14400s) |
imageSizes | included 16 | 16 removed from the default array |
qualities | all allowed | [75] alone |
maximumRedirects | unlimited | 3 |
| Local IP | allowed | blocked, absent dangerouslyAllowLocalIP |
| Local query string | free | demands 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
| Removed | Replacement |
|---|---|
AMP support (next/amp, useAmp, amp config) | None |
The next lint command | Biome or ESLint directly. next build runs lint no more |
serverRuntimeConfig and publicRuntimeConfig | Environment variables, with NEXT_PUBLIC_ for the client |
devIndicators.appIsrStatus, buildActivity, buildActivityPosition | The indicator itself remains |
unstable_rootParams | next/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:
// 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:
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:
@AGENTS.mdAnd 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
- Node 20.9 and TypeScript 5.1 in the environment and in CI. Before anything.
- Run the three codemods:
upgrade,next-async-request-api,next-lint-to-eslint-cli. - Decide on Turbopack before running the build. If you have a webpack config, choose between migrating and
--webpack. - Check edge before accepting the
proxyrename. It is the least reversible decision on the list. next buildand resolve what shows up. Parallel routes with nodefault.jsappear here.- Check the six
next/imagechanges in production, since none breaks the build and all change the result. - Test navigation with smooth scroll, if you use it.
cacheComponentswaits. 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
- Next.js — How to upgrade to version 16. Version 16.3.1, doc updated 18 August 2026. Accessed 20 August 2026.
- Next.js — Migrating to Cache Components. Doc updated 7 August 2026. Accessed 20 August 2026.
- Next.js — How to set up your Next.js project for AI coding agents. Doc updated 5 August 2026. Accessed 20 August 2026.
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.
Read next
Motion •
Motion Design for the Web: The Complete Guide
Scroll, text, images and video: the complete catalog of motion techniques for the web, with implementation in Next.js and the cases where each one pays off.
- motion
- scroll
The definitive guide — a Next.js site built around motion and scroll
The scroll foundation that, when missing, keeps the animations from working at all: Lenis, GSAP and Next.js wired in the right order and the mistakes to avoid.
- next.js
- lenis
Infra •
Documentation: deploying a Next.js application with GitHub + Hostinger
Every push becomes a live site with no hosting panel involved: connecting GitHub to Hostinger, the build settings that break and the checks after each deploy.
- deploy
- github


