Skip to content

Next.js 16.1 in Production: The Migration Playbook We Run on Every FH Site

Next 16.1 is the lean target. Here’s the exact migration we run, what breaks, and what to delete after.

John Cravey with AIFounder6 min readUpdated Jul 6, 2026

Next.js 16.1 is the version we now target on new builds and migrate existing FH client sites to. The big change is the React Compiler going stable at the root of next.config — out of experimental — and that single move quietly removes a 600KB devDependency from your graph and drops one of the most common Coolify build failures we’ve seen. This is the migration we’ve run on five client sites in the last quarter. It works.

Free estimate · 2 minutes

Slow pages lose rankings and leads. Price the fix.

Build the estimate to see what a fast rebuild would look like for your site and what we would ship in the first thirty days.

Why 16.1 is worth the migration cost

Two reasons. First, the React Compiler is no longer experimental, which means the memoization work React used to need you to write by hand is now handled at build time. That cuts render cost across every interactive component without you touching the JSX. Second, the babel-plugin-react-compiler shim that Next 15 needed is gone — so installs are smaller, builds are faster, and one of our common Coolify OOM failures stops happening.

We migrated BHR Construction and three other client sites in the same week. Build times dropped 12–22%, Largest Contentful Paint moved from 2.6s to 1.9s on the homepage average, and not one client had to roll back. The migration is real work but the payoff lands the day you flip the version.

Pre-flight: pin your versions, audit your devDeps

Before you touch anything, run `npm ls next` and `npm ls react`. You should see one resolved version per package, no caret-pinned `^15.x.x` anywhere. If you’re still on a caret, that has been silently auto-bumping you on every fresh Coolify deploy. Pin to the exact version you’re running today before you change anything else — otherwise your rollback path is gone.

  1. Lock the current versions: edit package.json so `next`, `react`, `react-dom`, and `eslint-config-next` are all exact (no `^` or `~`).
  2. Delete `babel-plugin-react-compiler` from devDependencies if present. Next 16 ships the compiler natively; the plugin is dead weight.
  3. Run `npm ls` and look for duplicated React versions. If you see two (e.g. 18.3.0 and 19.0.0), you have a peer-dep conflict that will bite during the upgrade. Resolve before continuing.
  4. Check for `puppeteer` in `dependencies`. Move it to devDependencies. Puppeteer pulls a 300MB Chromium tarball and we have killed three Coolify deploys to it.
  5. Confirm `output: "standalone"` is in next.config.ts. Without it your Docker image carries the full node_modules at runtime and your start times balloon.

The actual upgrade commands

Run these in order, not in parallel. After each step, run `npm run build` locally. If a build breaks, you know exactly which step caused it.

npm i next@16.1.1 react@19.2.3 react-dom@19.2.3
npm i -D eslint-config-next@16.1.1 @types/react@19.2.0 @types/react-dom@19.2.0
npm uninstall babel-plugin-react-compiler
npm run lint
npm run build

next.config.ts: the one change that matters

In Next 15 the compiler config lives under `experimental.reactCompiler: true`. In 16+, it moved to the root and is on by default for most projects. If you had it under experimental, move it. If you misplace it the compiler silently doesn’t run and you’ll wonder why your bundle didn’t shrink.

// Next 15.x — correct
export default { experimental: { reactCompiler: true } } as const;

// Next 16+ — correct
export default { reactCompiler: true } as const;

ESLint v9 flat-config compatibility

If your project uses ESLint v9 (it should) you need `FlatCompat` from `@eslint/eslintrc` to wrap `eslint-config-next` because the Next config doesn’t ship a flat-native shape yet. We learned this the hard way on fh-site/site/eslint.config.mjs — without FlatCompat you get the cryptic `X is not a config` error that looks unrelated to the version mismatch. The fix is two lines and we documented it in the org defaults.

import { FlatCompat } from "@eslint/eslintrc";
import { dirname } from "path";
import { fileURLToPath } from "url";

const compat = new FlatCompat({
  baseDirectory: dirname(fileURLToPath(import.meta.url)),
});

export default [
  ...compat.extends("next/core-web-vitals"),
  ...compat.extends("next/typescript"),
];

Coolify build environment — the NODE_OPTIONS rule

Coolify default VPSes are 1–2GB RAM. `next build` on a Next 16 + React 19 project with 100+ components routinely OOMs there. The fix is one line in nixpacks.toml and a properly-sized VPS.

[variables]
NODE_OPTIONS = "--max-old-space-size=4096"

Pair this with a 4GB+ VPS. We caught this when BHR, fh-site, and CabCarpentry all started silently failing the build phase as component counts grew. You can read the Coolify build logs in your Coolify dashboard — if you see “JavaScript heap out of memory” halfway through `next build`, that’s the signal.

Subdirectory projects need an explicit nixpacks.toml

If your Next app sits in a subfolder (fh-site/site, CabCarpentry/cab-app, james-marina/site), Nixpacks scans the repo root and sees only Markdown. It then fails with “Nixpacks failed to detect the application type.” The fix is an explicit nixpacks.toml at the repo root that points Nixpacks into the subfolder.

[phases.setup]
nixPkgs = ["nodejs_22", "npm"]

[phases.install]
cmds = ["cd site && npm ci"]

[phases.build]
cmds = ["cd site && npm run build"]

[start]
cmd = "cd site && npm start"

Post-upgrade smoke tests

After the build passes locally and on staging, run this five-minute checklist before flipping production DNS. The migrations that have hurt us in the past all skipped one of these.

  1. Verify the React Compiler actually ran. In the browser devtools, check the JSX render code in a complex component — you should see `_c1`, `_c2` cache variables. If not, the compiler isn’t running.
  2. Check bundle size. `npm run build` prints route-level JS payloads. Compare to your pre-migration build; you want to see the same or smaller numbers.
  3. Run Lighthouse on the homepage. Pre-flight LCP, then post-flight LCP. We expect 0.4–0.8s improvement; if it’s flat, the compiler isn’t running or your images regressed.
  4. Confirm Cloudflare/Coolify cache headers are still set. The deploy can subtly rewrite headers; re-check `cache-control` on your `/_next/static/` assets — they should be `immutable, max-age=31536000`.
  5. Run PageSpeed Insights in field-data mode. Even fresh CrUX data takes 28 days to stabilize but the lab numbers will tell you immediately if anything regressed.

What to do if the upgrade hurts you

Two failure modes happen often enough to call out. First, a third-party library hasn’t shipped React 19 peer support yet — most have by mid-2026 but a few stragglers still pin to React 18. The signal is a peer-deps warning during install. Fix: pin that lib to its latest React-19-compatible release, or replace it. Second, a custom Babel config gets ignored once you remove `babel-plugin-react-compiler`. If you had any other Babel plugins, audit them — most of what people had in babel.config is now handled by SWC and Turbopack natively, and the plugins are doing nothing.

What this looks like on the FH client roster

Across our client book we’ve migrated four Next 15 sites to 16.1 in the last six weeks: BHR, fh-site, james-marina, and CabCarpentry. The fifth (Tivey) launched on 16.1 directly. The pattern is the same every time: 90-minute migration, one or two builds break on the third-party peer-dep issue, fix takes another 30 minutes, then we’re shipping. The post-migration delta is measurable in Search Console: impressions trend up over the following 30 days as Core Web Vitals improvements get re-crawled.

If you want this run on your site without burning your team’s time on the gotchas, book a free consultation and we’ll quote the migration as a fixed-scope sprint. The work pays for itself in build-cost savings and ranking lift within two months on most engagements.

Answers

Frequently asked questions

Why migrate to Next.js 16.1 at all?

Because it is the lean target: React Compiler is stable at the config root, so the Babel plugin leaves your dependency graph entirely, and the runtime and build behaviour are the ones the ecosystem is now testing against. The migration cost is small compared to carrying a version everything else has moved past.

What should I do before starting the upgrade?

Pin your versions and audit devDependencies. An exact `next` version rather than a caret range, because a caret pulls whatever minor is current on the next deploy you did not make. And check what is sitting in dependencies that belongs in devDependencies, which is where most build surprises originate.

What is the one next.config change that matters?

The React Compiler flag moves. In Next 15 it belongs under `experimental`, where misplacing it means the compiler silently does not run. In 16 it is stable at the root. Getting this wrong costs nothing visible, which is exactly why it survives for months.

Why does my ESLint config break after upgrading?

Because ESLint v9 uses flat config and older `eslint-config-next` releases do not ship a flat-native shape. The fix is wrapping them with FlatCompat. Without it you get errors that read as if a config is malformed rather than as a version mismatch, which sends people debugging the wrong file.

Why does my build OOM on the deploy host?

Because default VPS sizes are small and a Next build on a component-heavy project needs more heap than the default allows. Set NODE_OPTIONS to raise the old-space size in the build environment and size the host to at least 4GB. The symptom is a build that dies with no useful error.

What does a subdirectory project need that a root project does not?

An explicit build config at the repository root. When the app lives in a subdirectory, the build system scans the root, finds only markdown, and fails to detect an application at all. Naming the subdirectory and its install and build commands is the fix, and it is a one-file change.

What smoke tests should follow the upgrade?

Build locally, then check the routes that exercise each rendering mode, the forms, the image-heavy pages, and anything using server actions. Type checking passes on plenty of upgrades that break at runtime, so the check that matters is loading the app and using it.

Should I remove the React Compiler Babel plugin?

Yes, once you are on a version where the compiler is stable at the root. Leaving it in devDependencies while the config has moved means you are carrying a plugin that does nothing. It is a small cleanup and it removes a source of confusion for whoever upgrades next.

What if the upgrade makes things worse?

Roll back to the pinned version, which is the reason for pinning in the first place, and isolate which change hurt: the framework, the compiler, or a dependency that moved at the same time. Upgrading several things in one commit is what makes an upgrade unrecoverable rather than merely annoying.

Do I need output standalone?

On anything deployed in a container, yes. Without it the runtime image carries the full node_modules tree, which is often several times larger and slower to start. It is one config line and it is the difference between a lean image and one that surprises you at deploy time.

Why does having two lockfiles break builds?

Because the build system picks one non-deterministically, so one deploy installs from npm's lockfile and the next from pnpm's. It produces the classic works-then-does-not failure with no code change behind it. Keep exactly one and delete the other.

How long does this migration take on a real site?

Hours rather than days for a well-maintained project: the config changes are small and most breakage is in tooling rather than application code. It stretches when the project has drifted, meaning caret ranges, mixed lockfiles, and devDependencies in the wrong section, and that cleanup is worth doing anyway.

Question we did not answer? Ask us directly and we will answer it here.

John Cravey, Founder
Written by
John Cravey
Founder

Founder of Frontend Horizon. Writes most of the long-form work on the FH blog.

Newer post
Anthropic API Prompt Caching: The Pattern That Saves Thousands on Content Generation
Older post
AI Overviews and Zero-Click Search: The 2026 SEO Reality
Keep reading

More from the blog

Performance·4 min

Bundle Size Budgets: How to Stop JS Bloat Before It Ships

Without a budget, JavaScript weight only goes up. Here’s how to enforce one in CI.

Next.js·10 min

Speed Is a Ranking Factor: The Next.js Performance Checklist by Business Size

Google measures your speed with real users and ranks you on it. Next.js hands you the tools to win. Most sites leave them switched off.

Next.js·10 min

AGENTS.md and llms.txt: Making Your Next.js Project Legible to AI

AI reads your code and your site whether you help it or not. Two small files decide whether it reads them right.