← the writing notes 9 min

Upgrading to Next.js 16: My Real Migration Notes

I've migrated three of my own production Next.js sites to version 16. Some of it was smooth. Some of it cost me a Friday evening. Here's exactly what I found, without the sugarcoating.

A glowing amber terminal on a developer's desk at night, empty coffee mug beside a worn keyboard, rain-streaked window in background

Three weeks ago I was sitting in our Seahawk office on a Thursday afternoon, fairly confident the upgrade from Next.js 15 to 16 on one of my personal portfolio sites would take forty minutes tops. It took the rest of the day. And honestly? The changelog doesn't quite prepare you for the parts that actually break.

I've built and shipped over 12,000 sites at this point. I'm not saying that to flex, I'm saying it because I've done enough of these migrations to know when the official upgrade guide is glossing over something. Next.js 16 glosses over a few things. So here are my actual notes, written the way I wish someone had written them for me.

---

Why I Bothered Upgrading at All

Turbopack. That's the short answer.

The longer answer is that two of my sites were sitting on Next.js 14, one was already on 15, and I'd been watching the Turbopack story unfold for about eighteen months. Version 16 is the first release where Turbopack is on by default for next dev. That's not a minor detail. On a mid-sized e-commerce build I did for a fashion client last year, cold start times in development were brutal, we're talking 12 to 18 seconds on first load. If Turbopack genuinely cuts that down, it's worth the migration pain.

Spoiler: it does cut it down. On the same class of project, I'm seeing 3 to 4 seconds cold. That's real.

But the path to get there has some sharp edges.

---

The Actual Upgrade Command (and What to Run First)

Before you touch anything, run a full audit of your current config. I use npx @next/codemod@latest religiously now. It won't catch everything but it catches the obvious renames and deprecated API calls. Run it, commit the output, then bump your package.json.

The upgrade itself:

  1. Update next , react , and react-dom to their target versions in package.json
  2. Run npm install (or pnpm install if you're sane, I've been on pnpm for two years)
  3. Run the codemod: npx @next/codemod@latest upgrade
  4. Boot next dev and read every warning before touching a single component
  5. Fix config issues before fixing component issues, order matters here

The codemod step is where I've seen people trip up. They skip it, hit three separate errors, and spend an hour hunting down things that would have been auto-fixed. Don't skip it.

---

next.config.js Changes That Will Bite You

This was the first real surprise for me. The configuration API shifted more than I expected.

The `experimental` block is slimmer now

Several flags that lived in experimental for the past year or two have either been promoted to stable (and moved to the top level) or removed entirely. The two I ran into personally:

  • experimental.appDir, gone. App Router is just the default now. If you have this in your config, it'll throw a warning (and in some setups, an outright error).
  • experimental.serverComponentsExternalPackages , promoted to serverExternalPackages at the top level of your config.

That second one caught me on a site that uses Prisma. The build was failing silently on the server bundle and I spent probably forty minutes staring at the wrong file before I spotted it. Check your next.config.js top to bottom before you assume a component is at fault.

Turbopack config lives in a new place

If you had any custom Webpack config and you're switching to Turbopack (which you will be, since it's now default for dev), you need to know that your webpack() function in next.config.js does not apply when Turbopack is running. It only applies during next build, which still uses Webpack.

This matters if you had custom SVG handling (I use SVGR on most projects), custom module aliases, or any loader config. You'll need to replicate those in the new turbopack config block. The Next.js Turbopack configuration docs are actually decent on this one, worth a read before you assume something is broken.

---

React 19 Compatibility: The Quiet Landmine

Next.js 16 ships with React 19 as its peer dependency. If you're upgrading from Next.js 14 (skipping 15), you're jumping two React major versions at once. That's where things get spicy.

The biggest issue I hit was with older third-party component libraries. I had a client site using a table library that internally used ReactDOM.render(). React 19 removed that API completely, it was deprecated back in React 18 but it still worked. In 19, it throws. Hard.

I spent a Tuesday morning on this one. The error message doesn't immediately point you at the library; it just tells you ReactDOM.render is no longer supported . Run npm ls react to see which packages in your tree have conflicting React peer dependency declarations. That command alone saved me probably two hours of guessing.

A few patterns worth knowing about for React 19 specifically:

  • forwardRef is no longer required for passing refs; refs are now a regular prop. Old components using forwardRef still work, but you'll see deprecation warnings.
  • use() is stable now and genuinely useful for async data in client components. I've started using it in preference to useEffect + state for straightforward fetches.
  • Server Actions have tighter type requirements. If you had anything loosely typed in your action signatures, TypeScript will now find it.

---

App Router: What Shifted in Caching Behaviour

This one is subtle and it will catch you in production if you're not paying attention.

Back in Next.js 14, fetch() inside Server Components was cached aggressively by default. You had to opt out with { cache: 'no-store' }. In Next.js 15 they reversed this (fetch is uncached by default), and Next.js 16 continues that direction with a few more explicit controls.

If you migrated from 14 to 16 in one jump (like I did with one of my sites), your pages that relied on the old default caching behaviour will start doing live fetches on every request. For some pages, that's fine. For others, it'll hammer your API and tank your response times.

The fix is explicit: use export const revalidate = 3600 (or whatever interval makes sense) at the route segment level, or pass { next: { revalidate: 3600 } } directly in your fetch call. The Next.js caching documentation has a solid breakdown of what caches what and when.

I audited every data-fetching route on the affected site using a quick grep for fetch( and added explicit caching declarations. Took about two hours but it was worth it, response times went from ~800ms average back down to ~120ms after the fix.

---

Turbopack in Practice: The Good and the Annoying

Let me be straight with you: Turbopack is impressive. Cold start times are dramatically better. Hot module replacement feels nearly instant on most changes. For day-to-day development it's a meaningful quality-of-life upgrade.

But there are rough edges.

What doesn't work yet

At the time I did these migrations, a handful of things still weren't fully supported under Turbopack for dev:

  • Some Webpack-specific loaders have no Turbopack equivalent yet. SVGR needed a config change (the Turbopack rules syntax is different from Webpack's module.rules).
  • Custom Babel transforms. Turbopack uses SWC only. If your project has a .babelrc or babel.config.js with custom plugins, those won't run. This is a known limitation and the Vercel team is upfront about it in their Turbopack docs.
  • A few PostCSS plugin combinations behave unexpectedly in dev. I saw this on a Tailwind v4 + custom PostCSS setup, the fix was pinning the PostCSS plugin order explicitly.

The `--turbopack` flag is now unnecessary

Since Turbopack is default for next dev in version 16, you don't need the --turbopack flag anymore. If you have it in your package.json scripts from experimenting with it in version 15, it won't hurt anything, but it's redundant. Tidy your scripts.

---

TypeScript and ESLint Config Updates

Two housekeeping things that tripped me up.

Next.js 16 moved to requiring TypeScript 5.x. If you're still on TypeScript 4.x (and some older projects are), you need to upgrade that separately. Run npx tsc --version before you start anything else.

The ESLint config story also changed. Next.js 16 ships with ESLint 9 support, and ESLint 9 uses a flat config format (eslint.config.js ) rather than the old .eslintrc format. If you're still on the old format, Next.js will fall back gracefully, but you'll see a warning. I migrated two of my projects to the flat config while I was in there anyway. It's honestly cleaner once you get over the initial friction.

---

My Migration Checklist (In Order)

This is what I'd hand to anyone on my team doing this upgrade:

  1. Back up your current config files and lock file before touching anything
  2. Check your Node.js version, Next.js 16 requires Node 18.18 or later
  3. Run npx @next/codemod@latest upgrade on the current version first
  4. Bump next , react , react-dom versions in package.json and install
  5. Review next.config.js for promoted or removed experimental flags
  6. Run npm ls react to spot third-party library conflicts
  7. Grep your codebase for fetch( and audit caching declarations
  8. Check for any .babelrc or Webpack-specific loaders that need Turbopack equivalents
  9. Boot dev, read all warnings before touching components
  10. Run a production build locally (next build) before deploying anywhere
  11. Deploy to a staging environment and do a full manual smoke test

That last step sounds obvious. But I've seen people skip staging and push straight to production on "small" upgrades. A caching behaviour change that makes your homepage hit a live API on every request is not a small thing.

---

FAQ

Is Next.js 16 stable enough for production?

Yes, for most use cases. The Turbopack-for-dev change is the biggest shift, and since production builds still use Webpack, your actual deployed output is less affected than your dev experience. The caching behaviour changes are the bigger production concern, and those are straightforward to audit if you're methodical about it.

Do I need to upgrade React to 19 at the same time?

Technically Next.js 16 supports React 18 as a minimum, but the new features (like the stable use() hook and the ref-as-prop change) require React 19. If you're on a project with a lot of third-party dependencies, it's worth checking compatibility before committing to React 19 at the same time. The React 19 upgrade guide is worth reading alongside the Next.js migration docs.

My custom Webpack config is gone under Turbopack. What do I do?

Your Webpack config still runs during next build . For dev, you need to replicate the relevant parts using the turbopack key in next.config.js. The syntax is different, especially for file transforms and aliases. Check the official Turbopack config reference and expect to spend an hour or two on it if your Webpack config is complex.

How much faster is Turbopack actually?

On the projects I've tested: cold start dropped from 12-18 seconds to 3-4 seconds. HMR on component changes went from 1-3 seconds to under 200ms in most cases. These are rough numbers and will vary with project size, but the difference is noticeable on anything beyond a toy project.

---

The upgrade is worth doing. Turbopack's dev speed alone changes how you feel about working in a large Next.js codebase. Just go in with your eyes open about the caching changes and the third-party library compatibility checks, those two things are where most of the time actually goes.

Take it one site at a time. I did.

Need this done, not just read?

start a project book 30 minutes