DEV Community

Ahmed Mahmoud
Ahmed Mahmoud

Posted on • Originally published at eng-ahmed.com

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

Headline: I upgraded two real apps — this portfolio and an internal operations dashboard — from Next.js 15 to 16. Total time: about half a day each. Almost none of it went where I expected. The codemod handles the boring parts; the surprises live in your config files and your CI scripts.

Next.js 16 is the release where the framework stops carrying its webpack-era luggage. Turbopack becomes the default bundler for both dev and build, the old escape hatches (sync params, next lint, runtime config) are gone rather than deprecated, and middleware.ts gets a new name that finally says what it does.

This is not a changelog rehash. It's the ordered list of what actually broke for me, with the fix and the time each one cost.

The 30-second upgrade path

npx @next/codemod@canary upgrade latest
# then read your build output very carefully
Enter fullscreen mode Exit fullscreen mode

The codemod updates next, react, and react-dom, and rewrites the mechanical stuff — most notably async request APIs. Run it on a clean branch. Everything below is what it didn't catch for me.

1. Turbopack is now the default — your webpack config is dead weight

The single biggest behavioral change: next dev and next build both run Turbopack unless you opt out. On my dashboard, a leftover webpack() block in next.config.ts triggered a hard warning that the config would be ignored.

Three possible states you can be in:

  1. No custom webpack config. You're done. Enjoy builds that are dramatically faster — my portfolio's production build dropped from 41s to 19s.
  2. Custom webpack config you can replace. Most loaders have a Turbopack equivalent under turbopack.rules. SVG-as-component via @svgr/webpack was a five-minute port for me.
  3. Custom webpack config you can't replace yet. Opt out per command with next build --webpack and file the migration for later. Don't let one loader block the whole upgrade.

2. middleware.ts is now proxy.ts

The rename is cosmetic; the mental model shift isn't. The file always ran as a network-boundary interceptor, and the new name stops people from treating it like an application-layer hook. Rename the file, rename the exported function to proxy, done:

// proxy.ts (was middleware.ts)
import { NextResponse } from 'next/server';

export function proxy(request: Request) {
  // same matcher config, same semantics
  return NextResponse.next();
}
Enter fullscreen mode Exit fullscreen mode

My auth-gate logic moved over without a single behavioral change. Ten minutes, including updating two imports in tests.

3. next lint is gone — and your CI will notice before you do

This one bit me in CI, not locally. The lint script in both repos called next lint, which no longer exists in 16. The fix is to run ESLint directly:

{
  "scripts": {
    "lint": "eslint app src --max-warnings 0"
  }
}
Enter fullscreen mode Exit fullscreen mode

While you're in there: Next 16 pairs with the flat ESLint config world. If you're still on .eslintrc, budget 20 minutes to move to eslint.config.js and eslint-config-next's flat preset.

4. Async params and searchParams: the grace period is over

Next 15 let you access params synchronously with a deprecation warning. Next 16 removes the sync path entirely — every dynamic API is a Promise, full stop:

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  // ...
}
Enter fullscreen mode Exit fullscreen mode

The codemod converted all of my 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; runtime would have too, loudly. Lesson — after the codemod, grep for params. and audit anything that isn't awaited.

5. Image defaults tightened

Two changes landed in next/image that show up as subtle regressions rather than errors. Quality values now come from an allowlist (images.qualities, default [75]) — if a component asked for quality={90}, it silently serves 75 until you add 90 to the config. And local images with query strings stopped resolving on one of my pages until I registered the pattern under images.localPatterns.

// next.config.ts
const nextConfig = {
  images: {
    qualities: [75, 90],
  },
};
Enter fullscreen mode Exit fullscreen mode

6. 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 any self-hosted runtime — my VPS deploy script was still provisioning Node 18 and failed the engines check immediately. React 19.2 itself was a non-event for app code: both apps were already on 19.x, and nothing in the minor broke.

What you get for the trouble

  • Build speed. 41s → 19s on the portfolio, 68s → 24s on the dashboard (cold, no cache). Turbopack's filesystem cache for dev is behind a flag and makes warm starts near-instant.
  • Cache Components. The use cache model keeps maturing in 16 — explicit, composable caching instead of the implicit fetch-cache guessing game.
  • A smaller framework surface. AMP support, runtime config, and the legacy lint wrapper are gone. Less magic, fewer docs pages that lie to you.

The checklist I'd hand a teammate

  1. Clean branch, run the codemod, commit the diff separately from your manual fixes.
  2. Delete or port the webpack() block; only reach for --webpack if something genuinely has no Turbopack path.
  3. Rename middleware.tsproxy.ts, export proxy.
  4. Replace next lint in every script and CI job.
  5. Grep for non-awaited params/searchParams in helpers, not just pages.
  6. Audit next/image usage for non-default quality values.
  7. Bump Node to ≥20.9 everywhere your app runs, including CI and deploy targets.

Half a day per app. The build-speed win alone paid that back within the first week of development. Upgrade sooner rather than later — 16 removes deprecations, which means 17 will remove the things 16 just deprecated.


More writing on eng-ahmed.com. Built by Devya Solutions.

Top comments (0)