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.
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.
- Lock the current versions: edit package.json so `next`, `react`, `react-dom`, and `eslint-config-next` are all exact (no `^` or `~`).
- Delete `babel-plugin-react-compiler` from devDependencies if present. Next 16 ships the compiler natively; the plugin is dead weight.
- 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.
- Check for `puppeteer` in `dependencies`. Move it to devDependencies. Puppeteer pulls a 300MB Chromium tarball and we have killed three Coolify deploys to it.
- 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 buildnext.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.
- 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.
- 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.
- 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.
- 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`.
- 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.