Skip to main content
Upgrading to Next.js 16: Field Notes from Two Production Apps

Upgrading to Next.js 16: Field Notes from Two Production Apps

July 6, 2026
Web Development
5 min read

At Devya Solutions we recently upgraded two production applications — a client-facing web platform and an internal operations dashboard — from Next.js 15 to 16. Total time: about half a day each. Almost none of it went where we expected. The official codemod handles the boring parts; the surprises live in config files and CI scripts.

Next.js 16 is the release where the framework finally sheds its webpack-era luggage. Turbopack becomes the default bundler for both dev and build, the old escape hatches (synchronous params, next lint, runtime config) are removed rather than deprecated, and middleware.ts gets a new name that finally says what it does. This is not a changelog rehash — it is the ordered list of what actually broke for us, with the fix and the time each one cost.

The 30-second upgrade path

The upgrade starts with a single command: npx @next/codemod@canary upgrade latest. The codemod updates next, react, and react-dom, and rewrites the mechanical parts — most notably the async request APIs. Our advice: run it on a clean branch, then read your build output very carefully. Everything below is what the codemod did not catch for us.

Turbopack is now the default

The single biggest behavioral change: next dev and next build both run Turbopack unless you opt out. On our dashboard, a leftover webpack() block in next.config.ts triggered a hard warning that the config would be ignored. There are three states a project can be in. If you have no custom webpack config, you are done — and builds get dramatically faster: one of our production builds dropped from 41 seconds to 19. If you have a config you can replace, most loaders have a Turbopack equivalent under turbopack.rules — porting SVG-as-component took us five minutes. And if something genuinely has no Turbopack path yet, you can opt out per command with next build --webpack and schedule the migration. We never let one loader block a whole upgrade.

middleware.ts is now proxy.ts

The rename is cosmetic; the mental-model shift is not. The file always ran as a network-boundary interceptor, and the new name stops teams from treating it like an application-layer hook. Rename the file, rename the exported function to proxy, and you are done — our auth-gate logic moved over without a single behavioral change, in about ten minutes including test imports.

next lint is gone — your CI will notice first

This one bit us in CI, not locally. The lint script in both repositories called next lint, which no longer exists in 16. The fix is running ESLint directly in package scripts. While you are in there, note that Next 16 pairs with the flat ESLint config world — if a project is still on .eslintrc, budget twenty minutes to move to eslint.config.js.

Async params: the grace period is over

Next 15 allowed synchronous access to params with a deprecation warning. Next 16 removes the sync path entirely — every dynamic API is a Promise. The codemod converted all of our route files correctly. What it missed: a helper function that received params as an argument and destructured them synchronously two levels down. TypeScript caught it before runtime did. Our lesson: after the codemod, grep the codebase for params usage and audit anything that is not awaited.

Image defaults tightened

Two changes in next/image show up as subtle regressions rather than errors. Quality values now come from an allowlist (images.qualities, default [75]) — a component requesting quality={90} silently serves 75 until 90 is added to the config. And local images with query strings stopped resolving on one page until we registered the pattern under images.localPatterns.

The floor moved: Node 20.9+, React 19.2

Next 16 requires Node.js 20.9 or newer and ships against React 19.2. The Node bump is the one to check in CI images and self-hosted runtimes — one of our deploy scripts was still provisioning Node 18 and failed the engines check immediately. React 19.2 itself was a non-event for application code.

What you get for the trouble

Build speed first: 41s to 19s on one app, 68s to 24s on the other, cold with no cache. Cache Components keep maturing — explicit, composable caching instead of the implicit fetch-cache guessing game. And a smaller framework surface: AMP support, runtime config, and the legacy lint wrapper are gone. Less magic, fewer docs pages that lie to you.

Our upgrade checklist

Run the codemod on a clean branch and commit that diff separately from manual fixes. Delete or port the webpack() block. Rename middleware.ts to proxy.ts. Replace next lint in every script and CI job. Grep for non-awaited params in helpers, not just pages. Audit next/image for non-default quality values. Bump Node to 20.9+ everywhere the app runs. Half a day per app — the build-speed win alone paid that back within the first week of development.

If your team is planning a Next.js upgrade — or is stuck on an older major and wants the migration done safely — this is exactly the kind of work we do at Devya Solutions. Get in touch and we will scope it with you.