Next.js 16: What Changed Since 15.5?
In September 2025, I wrote about Next.js 15.5, including my migration from ESLint to Biome.
Since then, Next.js has gone through several releases and reached version 16. There have been changes to the bundler, caching, routing, React integration, and even some of the assumptions behind a typical Next.js project.
So, a year later, it's worth taking another look.
This isn't a complete changelog. Instead, I'll focus on the changes that matter most when moving an existing project from Next.js 15.5 to 16.
TL;DR
Turbopack is now the stable default for both next dev and next build - a custom Webpack config can break your build unless you migrate it or explicitly pass --webpack. Node.js 20.9+ is required, next lint and its next.config.js option are gone (Biome or ESLint directly now), and synchronous access to cookies(), headers(), params, and searchParams no longer works. middleware still works but is deprecated in favor of proxy, which drops Edge runtime support. Cache Components and Instant Navigations are real architectural shifts, but both are opt-in behind cacheComponents: true and partialPrefetching: true - nothing changes automatically just by upgrading. React Compiler is stable but off by default, and the App Router now runs on React's latest Canary rather than a plain stable release. Run npx @next/codemod@canary upgrade latest for the bulk of the migration, but run next-async-request-api separately - the main codemod doesn't cover it.
From 15.5 to 16.3
A quick overview:
- 15.5 — Turbopack builds in beta, Node.js middleware, TypeScript improvements
- 16.0 — Turbopack stable, Cache Components, React Compiler stable, Enhanced Routing (layout deduplication, incremental prefetching)
- 16.1 — Turbopack filesystem caching and development improvements
- 16.2 — Faster development and rendering, Server Fast Refresh
- 16.3 — Instant Navigations, lower memory usage, faster builds and rendering
There is a common thread between these releases: Next.js is moving more of its development workflow into Turbopack while making rendering, caching, and navigation more granular.
Turbopack is now the default
One of the biggest changes in Next.js 16 is that Turbopack became the stable default bundler.
In Next.js 15, you typically had to opt into it:
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build --turbopack"
}
}
With Next.js 16, this is no longer necessary:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}
Turbopack is now used by default for both development and production builds.
This is more than a change to a command-line flag. The development experience is increasingly built around Turbopack's incremental compilation, caching, and memory management.
If your project has a custom Webpack configuration, this is one of the things that will actually stop your build. next build uses Turbopack by default now, and an old Webpack-specific config isn't just "something to check" - it can fail outright. You have three ways out: run with --turbopack and let Next.js ignore the Webpack config, migrate the relevant options to the new Turbopack config format, or pass --webpack to explicitly opt back into Webpack for that build.
Turbopack configuration has also moved out of experimental:
const nextConfig = {
turbopack: {
// options
},
}
instead of:
const nextConfig = {
experimental: {
turbopack: {
// options
},
},
}
For a new project, most of this is invisible. For an older project, it is worth checking your configuration.
Faster Development Workflow
Performance is probably the easiest improvement to appreciate.
Next.js 16.2's release post leads with roughly a 400% faster next dev startup and about 50% faster rendering in its headline benchmarks. In the body of that post, the more specific number for a default application is closer to 87% faster startup compared to 16.1 - worth knowing so you don't repeat the headline figure as if it were the typical case.
Next.js 16.3 continued the work, with reported memory usage down by up to 90% in one benchmark (a project with around 50 routes going from roughly 21.5 GB down to about 2 GB after a full compile on vercel.com, with a similar but slightly smaller drop of around 82% measured on nextjs.org), along with faster builds and rendering.
These numbers are benchmarks rather than guarantees for every application, so the exact improvement will depend on the project.
Still, the direction is clear: the development server is becoming faster and less expensive to run, especially for larger applications.
Next.js 16 also changed how development and production output are separated. next dev now writes to .next/dev, which makes it possible to run next dev and next build concurrently.
That may seem like a small implementation detail, but it is another example of Next.js treating development as a first-class performance problem.
Caching Changed Again
Caching has probably been one of the more complicated parts of Next.js over the last few releases.
Next.js 16 introduced Cache Components, bringing together caching and Partial Prerendering into a more explicit model. This is opt-in, not automatic - you need to turn it on in next.config.js:
const nextConfig = {
cacheComponents: true,
}
The idea is to make different parts of a route behave differently instead of treating the entire page as either static or dynamic.
For example, a page might contain:
- a static shell;
- cached data;
- dynamic user-specific content.
The framework can then reuse the parts that don't need to be rendered again while streaming the dynamic content.
The API itself is relatively small:
import { cacheLife } from 'next/cache'
export async function getPosts() {
'use cache'
cacheLife('hours')
return db.posts.findMany()
}
The bigger change is architectural.
Instead of relying entirely on the framework to decide how a route should be cached, you can explicitly describe which pieces of your application should be cached and for how long. Worth knowing before you flip the flag: turning on cacheComponents can surface build errors for any dynamic data read outside a <Suspense> boundary, so it's not a purely additive change - expect to touch some existing routes.
Next.js 16 also removed the old experimental PPR configuration in favor of the flag above.
This is one of the areas where upgrading from an older Next.js can require more than changing the version number.
Enhanced Routing and Instant Navigations
It's worth separating two things that landed in different releases but both affect how navigation feels.
Enhanced Routing shipped in 16.0. This is layout deduplication and incremental prefetching, and it works automatically without any code changes. With layout deduplication, if several destinations share the same layout, Next.js downloads that layout once instead of fetching it repeatedly. With incremental prefetching, Next.js fetches only the parts of a route that aren't already cached. Less redundant work, faster navigation, no opt-in required.
Instant Navigations is a separate, later feature in 16.3, and it is opt-in - it requires both cacheComponents: true and partialPrefetching: true. It introduces three navigation modes Next.js can choose between for a given link: Stream (render with Suspense as data resolves), Cache (serve from a cached, prerendered version), or Block (wait for everything before navigating). Alongside that, Partial Prefetching means Next.js can prefetch and reuse a single shared shell across a route instead of prefetching the full destination for every link. The release also added Instant Insights and a Navigation Inspector for diagnosing what strategy was used on a given navigation, a Playwright helper (instant()) for testing navigation behavior, and improvements to ISR. Vercel's own framing is that this behavior is expected to become the default in a future major release, but as of 16.3 it's still behind those two flags.
This is an interesting direction for Next.js because it tries to combine two things that historically felt like separate approaches: server rendering and SPA-like navigation. You can keep Server Components and streaming while making transitions feel much more immediate - once you've turned the relevant flags on.
React 19.2 (via Canary)
Next.js 16's App Router runs on React's latest Canary channel rather than a plain stable 19.2 release - that Canary includes 19.2's shipped features plus some still-stabilizing ones, so it's worth being precise about what's actually stable versus still Canary-only.
Among the more interesting additions:
View Transitions
React can coordinate UI changes with browser View Transitions, making it easier to animate navigation and other updates. One caveat worth flagging: as of recent React documentation, <ViewTransition> itself is still Canary-only, not part of a stable release - so treat it as an experimental API in production code for now.
useEffectEvent
useEffectEvent lets you extract non-reactive logic from Effects without putting every value used by that logic into the Effect's dependency array.
<Activity />
<Activity /> allows React to keep background UI mounted while hiding it from the user and cleaning up Effects appropriately. This one did stabilize with 19.2.
These features aren't exclusively Next.js features, but Next.js 16 makes them part of the framework's current React environment.
React Compiler
React Compiler support is now stable in Next.js 16.
The compiler can automatically memoize components and values, reducing unnecessary re-renders without requiring developers to manually add useMemo and useCallback everywhere.
For example, code like this:
const value = useMemo(
() => expensiveCalculation(data),
[data]
)
may no longer need manual memoization when the compiler can safely perform the optimization itself.
However, React Compiler is not enabled by default.
You can enable it with:
const nextConfig = {
reactCompiler: true,
}
and install the compiler plugin.
There is also a trade-off: the compiler currently relies on Babel under the hood, which is why enabling it can increase development and build times - it isn't just "extra compilation work" in the abstract, it's specifically the Babel-based pipeline. Next.js 16.3 introduced an experimental Rust-based port of the compiler; in Vercel's own tests on v0, it cut time-to-interactive by roughly 34–46%. If build time with the compiler enabled is a real cost for you, it may be worth watching that experimental path rather than assuming the Babel-based compiler is the only option long-term.
So this is better treated as something to measure on an existing project rather than a switch that should automatically be enabled after upgrading.
What Actually Broke?
This is probably the most important part of the upgrade - and it's worth distinguishing genuine breaking changes from things that are merely deprecated but still work.
Node.js 20.9+
Next.js 16 requires Node.js 20.9 or later. Node.js 18 is no longer supported.
Before upgrading, check:
node --version
This is particularly important in CI, Docker images, and deployment environments.
next lint Is Gone
The next lint command was removed in 16. It's worth noting this wasn't a surprise: Next.js 15.5 already marked next lint as deprecated a year earlier, alongside the same release that pushed Biome as an alternative to ESLint - the removal in 16 is the other half of that same change I wrote about back then.
Instead, use ESLint or Biome directly:
{
"scripts": {
"lint": "eslint"
}
}
next build no longer runs linting either.
The eslint option in next.config.js was also removed.
This is especially relevant if you're upgrading a project that has been around for a few years.
In my case, it is a nice continuation of the migration I wrote about last year: I had already moved this project from ESLint to Biome, and Next.js has now removed its own linting abstraction altogether.
middleware → proxy (deprecated, not removed)
The middleware convention has been renamed to proxy, but this is a deprecation rather than a hard break - middleware.ts still works in 16. That said, it's worth treating it as something to migrate rather than ignore.
For example:
middleware.ts
becomes:
proxy.ts
and:
export function middleware(request: Request) {
// ...
}
becomes:
export function proxy(request: Request) {
// ...
}
The important detail: the new proxy convention only supports the Node.js runtime. If you specifically depend on the Edge runtime, proxy isn't a drop-in replacement - you need to keep using middleware until that gap is addressed, rather than migrating blindly.
The name change is mostly about making the purpose of this feature clearer: it sits at the network boundary rather than being general-purpose middleware.
Async Request APIs
Next.js 15 introduced asynchronous request APIs but temporarily kept synchronous compatibility.
Next.js 16 removes that compatibility.
These APIs must now be accessed asynchronously:
cookies()headers()draftMode()paramssearchParams
For example:
export default async function Page({
params,
}: PageProps<'/blog/[slug]'>) {
const { slug } = await params
return <h1>{slug}</h1>
}
If you haven't migrated these APIs yet, this is one of the areas most likely to produce errors during an upgrade. The general upgrade codemod does not handle this migration on its own - there's a dedicated one, next-async-request-api, that you need to run separately.
Parallel Routes Now Require default.js
If you use parallel routes and a slot doesn't have a matching default.js, the build now fails instead of silently falling back. If your project uses @slot conventions, this is worth checking explicitly before upgrading rather than discovering it at build time.
revalidateTag() Requires a Second Argument
Calling revalidateTag() with only a tag name is now a TypeScript error - it expects an additional argument. If your codebase calls this API anywhere, expect a type-checking failure until it's updated.
next/image Defaults Changed
Next.js 16 also changed several next/image defaults.
For example, the default minimumCacheTTL increased from 60 seconds to 4 hours.
The default allowed qualities are now restricted to [75], and local image URLs with query strings require explicit images.localPatterns.search configuration.
There are also changes to local IP optimization and maximum redirects.
Most applications won't notice these changes immediately, but image-heavy projects should review their configuration.
Other Removals
A few other deprecated features were removed:
- AMP support;
serverRuntimeConfigandpublicRuntimeConfig;- several old
devIndicatorsoptions; experimental.dynamicIO;experimental_ppr;unstable_rootParams- replaced in 16.3 by a stablenext/root-paramsimport, so this one has a direct migration path rather than just disappearing.
The official upgrade guide has the complete migration list.
TypeScript Configuration
The upgrade can also affect tsconfig.json.
A modern Next.js project uses:
{
"module": "esnext",
"moduleResolution": "bundler"
}
If you're upgrading an older project, you may see:
"moduleResolution": "node"
change to:
"moduleResolution": "bundler"
This is not a random change. bundler resolution is designed for modern bundlers and better reflects how tools such as Turbopack resolve modules. It also happens to line up with where TypeScript itself is heading - as I wrote in my piece on TypeScript 6.0 and 7.0, moduleResolution: node is one of the options TypeScript 7.0 removes outright, so this is a change you'll want to make regardless of which framework version pushes you toward it. Next.js 16.3 also added the ability for next build to use TypeScript 7 for type checking, so the two upgrades are worth planning together if you're touching both this year.
It's also a good opportunity to review old TypeScript configuration rather than blindly keeping everything that accumulated over the years.
Migrating My Project
This is where the upgrade becomes more interesting than a release-notes summary.
When I started the development server after upgrading, Next.js immediately pointed out some old configuration:
Invalid next.config.js options detected:
Unrecognized key(s) in object: 'swcMinify', 'eslint'
Removing swcMinify
An old configuration might contain:
const nextConfig = {
swcMinify: true,
}
Worth being precise here: this option's support was actually removed back in Next.js 15, not 16 - SWC minification has been the default and non-configurable since then. If you saw this warning, it likely would have appeared on 15 already; upgrading to 16 just made it impossible to keep ignoring.
Removing eslint from next.config.js
I also had an old ESLint configuration inside next.config.js. That one is genuinely tied to the 16 upgrade.
That needs to go as well.
Linting is now handled directly by ESLint or Biome rather than by Next.js.
Updating TypeScript module resolution
The project also changed:
"moduleResolution": "node"
to:
"moduleResolution": "bundler"
Again, this is a relatively small change, but it is a good example of how framework upgrades gradually remove assumptions from older projects.
The important thing is not to treat automatically modified configuration as something mysterious. Check what changed and why.
The Upgrade Is More Than a Version Bump
The biggest lesson from this upgrade is that a major framework update is rarely just:
npm install next@latest
Next.js provides an upgrade codemod that can handle several migrations automatically:
npx @next/codemod@canary upgrade latest
It can migrate things such as:
next lintto the ESLint CLI;middlewaretoproxy;- deprecated Turbopack configuration;
- stabilized APIs;
- old PPR configuration.
It does not, on its own, migrate the async request APIs (cookies(), headers(), params, searchParams) - that's a separate codemod, next-async-request-api, worth running explicitly rather than assuming upgrade covers everything.
But automated migration is only the beginning. You still need to check your configuration, run the application, test the build, and pay attention to warnings. It's also worth checking for the latest patch release once you're on 16 - Next.js has shipped point releases for security fixes on this line, so don't assume the first 16.x you land on is the one to stay on.
Bottom Line
A year after writing about Next.js 15.5, the biggest change isn't one individual API.
It's the direction of the framework.
Turbopack is now the default foundation of the toolchain. Caching and rendering are becoming more granular, though much of the newest routing behavior is still behind opt-in flags rather than default. React Compiler is ready for production use - with a faster experimental path already in testing - and React itself brings new capabilities through its latest Canary channel, some of which (like <ViewTransition>) aren't stable yet either.
At the same time, Next.js 16 removes quite a bit of old baggage, though not all of it in the way you'd expect. next lint is gone (but that was announced back in 15.5). Old runtime configuration is gone. Synchronous request APIs are gone. middleware still works, but is deprecated in favor of proxy - with a real gap around Edge runtime support. Several old configuration options, like swcMinify, were actually removed a version earlier than you might assume.
So if you're moving from Next.js 15.5 to 16, I wouldn't think of it as simply updating a dependency, and I also wouldn't take every "what changed" claim - including some of my own assumptions going into this - at face value without checking the release notes directly.
It's a good opportunity to clean up the project, remove old assumptions, and take advantage of the new development and rendering model - as long as you're clear on which parts of that model you actually have to opt into.
For me, that turned out to be one of the most useful parts of the upgrade: not just getting a newer version of Next.js, but finally cleaning up configuration that had been following the project around for years.