İçeriğe geç
Web Development

Next.js 16 Migration Guide: Step-by-Step Upgrade

Migrating to Next.js 16 is more than a version upgrade. Explore changes to Turbopack, async APIs, Proxy, caching, and image optimization.

Ertuğrul TokerLast Updated: 17 August 2026

Next.js 16 Migration Guide: Step-by-Step Upgrade

Updating a Next.js project between major versions is rarely as simple as running npm install next@latest and moving on. Especially in production applications that rely on authentication, middleware, caching, image optimization, or custom build configurations, upgrading the framework can directly affect the application's architecture.

Next.js 16 is exactly that kind of release.

First released in October 2025, Next.js 16 introduced Turbopack as the default bundler, changes to the caching model, React Compiler support, routing improvements, and several breaking changes that may require direct code modifications during migration. The framework has continued to evolve since then, with Next.js 16.3, released on August 3, 2026, bringing further improvements to performance and developer experience.

So instead of looking only at the question, "What's new in Next.js 16?", this guide focuses on a more practical concern:

What can break when migrating an existing Next.js 15 application to Next.js 16, and how can we manage the migration safely?

Start by Checking the Environment

Before starting a migration, the first thing I look at is not the framework itself but the runtime environment. That's because the minimum system requirements have also changed with Next.js 16.

Next.js 16 requires at least Node.js 20.9.0 and TypeScript 5.1.0. Node.js 18 is no longer supported.

First, check your current Node.js version:

node -v

If the project uses Docker, updating only your local environment is not enough. The Node image inside the Dockerfile, the CI/CD pipeline, and the Node.js version on the production server should also be reviewed.

For example, if your existing Dockerfile contains:

FROM node:18-alpine

you'll need to move it to a supported Node 20 version when upgrading to Next.js 16.

This is one of the easiest issues to overlook during major framework migrations: everything works locally while the production build environment is still running an outdated Node.js version.

How to Upgrade to Next.js 16

The Next.js team provides a codemod specifically for the upgrade process.

If you're using npm:

npx @next/codemod@canary upgrade latest

If you prefer to upgrade manually, the basic package update looks like this:

npm install next@latest react@latest react-dom@latest

For TypeScript projects, React type packages should also be updated:

npm install -D @types/react@latest @types/react-dom@latest

I generally prefer starting with the codemod whenever possible. The v16 codemod does more than update dependency versions. It can update Turbopack configuration, migrate next lint usage to the ESLint CLI, convert the middleware convention to proxy, and clean up some outdated experimental configuration.

However, there is an important distinction:

The codemod makes migration easier, but it does not complete the migration for you.

Custom Webpack configurations, third-party packages, authentication layers, and caching behavior should still be tested manually.

Turbopack Is Now the Default

One of the changes I would pay the most attention to when moving to Next.js 16 is the bundler.

Starting with Next.js 16, Turbopack is the default bundler for both next dev and next build. You no longer need to explicitly use the --turbo or --turbopack flags.

Previously:

{
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build --turbopack"
  }
}

Now:

{
  "scripts": {
    "dev": "next dev",
    "build": "next build"
  }
}

So far, so good. The challenge begins with projects that use custom Webpack configuration.

If your next.config.js contains a custom webpack() configuration, Next.js 16 switching directly to Turbopack may lead to unexpected behavior. In some cases, Next.js can stop the build rather than continue with an incompatible configuration.

If you need to continue using Webpack:

next dev --webpack
next build --webpack

Turbopack is now quite mature, but it still does not provide a one-to-one equivalent of the entire Webpack plugin ecosystem. Large projects that rely on Sentry integrations, custom loaders, custom Sass processing, or other Webpack plugins should therefore review these integrations before migrating.

Instead of taking the approach of "Turbopack is here, so I'll remove Webpack immediately," I would first compare development and production builds using both bundlers.

params, searchParams, cookies(), and headers() Are Fully Async

Next.js 15 had already started preparing developers for this change.

Request-time APIs such as params, searchParams, cookies(), headers(), and draftMode() became asynchronous in Next.js 15. However, synchronous access temporarily continued to work for backward compatibility.

That compatibility layer is gone in Next.js 16.

Synchronous access has now been completely removed.

For example, older code may look like this:

export default function Page({
  params,
}: {
  params: { slug: string }
}) {
  const { slug } = params

  return <div>{slug}</div>
}

With Next.js 16, it should be updated to:

export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params

  return <div>{slug}</div>
}

The same applies to cookies().

Previous usage:

import { cookies } from 'next/headers'

const cookieStore = cookies()
const token = cookieStore.get('token')

New usage:

import { cookies } from 'next/headers'

const cookieStore = await cookies()
const token = cookieStore.get('token')

In a large App Router project, this would be one of the first things I would search for after upgrading.

The change is not limited to page.tsx files. params may also be used in layouts, Route Handlers, generateMetadata, Open Graph image routes, and other parts of the application.

Next.js can also generate type helpers such as PageProps, LayoutProps, and RouteContext with:

npx next typegen

middleware.ts Is Being Replaced by proxy.ts

Another notable change is related to naming.

With Next.js 16, the middleware.ts convention has been deprecated in favor of proxy.ts. The goal is to make it clearer that this layer is not a general-purpose Express-style middleware but instead operates at the network boundary in front of the application.

Previously:

// middleware.ts

export function middleware(request: NextRequest) {
  // ...
}

The new structure:

// proxy.ts

export function proxy(request: NextRequest) {
  // ...
}

There is also a dedicated codemod for the migration:

npx @next/codemod@canary middleware-to-proxy .

This is not only a filename change. The exported middleware function is also expected to be renamed to proxy.

The more important change, however, is the runtime.

proxy.ts runs on the Node.js runtime, and its runtime cannot be manually changed. Existing Middleware implementations that depend on the Edge runtime therefore need to be evaluated separately during migration.

Projects that heavily use Middleware for authentication, locale redirects, rewrites, or authorization checks should test this layer carefully.

next lint Is Gone

This may look like a small change, but it can easily break CI pipelines.

Next.js 16 removes:

next lint

At the same time, next build no longer automatically runs lint checks. Next.js has separated linting from the framework build process and now expects projects to use the ESLint CLI directly.

So if your package.json contains:

{
  "scripts": {
    "lint": "next lint"
  }
}

you can update it to something like:

{
  "scripts": {
    "lint": "eslint ."
  }
}

The migration codemod can also help with this conversion.

If your CI/CD pipeline runs npm run lint before building, make sure this is included in your migration checklist.

next/image Defaults Have Changed

Next.js 16 also changes several defaults related to image optimization.

For example, the default value of images.minimumCacheTTL has increased from 60 seconds to 4 hours. Image quality configuration has also changed, with [75] now being the default allowed quality instead of the previous broader range.

If your project contains usage like:

<Image
  src="/product.jpg"
  width={800}
  height={600}
  quality={100}
  alt="Product"
/>

and you want to preserve quality={100}, you may need to explicitly configure it:

const nextConfig = {
  images: {
    qualities: [50, 75, 100],
  },
}

export default nextConfig

Another breaking change affects local image URLs with query strings.

For example:

<Image
  src="/assets/product.jpg?v=2"
  width={500}
  height={500}
  alt="Product"
/>

If you use URLs like this, you may now need to define an appropriate images.localPatterns configuration.

This is particularly important in e-commerce and CMS-based projects, where image URLs are often generated dynamically.

Cache Components Make Caching More Explicit

Caching has been one of the most confusing areas in recent major Next.js releases.

Questions such as "Why was this fetch cached?", "Why did this route become static?", and "Why didn't the revalidation appear immediately?" are familiar to almost everyone who has worked with the App Router.

Next.js 16 moves toward a more explicit caching model.

Cache Components can be enabled with:

const nextConfig = {
  cacheComponents: true,
}

export default nextConfig

and allow developers to control which components or functions are cached using the "use cache" directive. The previous experimental.ppr approach has also been moved into the Cache Components model.

There is one important point here:

You do not have to use Cache Components simply because you're migrating to Next.js 16.

During a migration, I would avoid redesigning the entire caching architecture in the same commit. I would first stabilize the framework upgrade and then handle the move to Cache Components as a separate task.

Combining a major version migration and a caching architecture rewrite makes it much harder to identify the source of a problem when something breaks.

revalidateTag() Has Changed, and updateTag() Has Arrived

There are also important API-level changes on the caching side.

The recommended approach for revalidateTag() now uses a second cacheLife profile argument:

import { revalidateTag } from 'next/cache'

revalidateTag('blog-posts', 'max')

This marks the existing cache as stale using a stale-while-revalidate strategy and allows fresh content to be fetched in the background.

The single-argument form is now deprecated.

Next.js 16 also introduces the updateTag() API.

'use server'

import { updateTag } from 'next/cache'

export async function updateProfile() {
  // database update

  updateTag('user-profile')
}

updateTag() is designed for cases where a user should immediately see the latest version of data after making a change. The API can only be used inside Server Actions.

I think of the distinction like this:

Use revalidateTag() for content such as blogs, catalogs, or documentation where a few seconds of delay is acceptable. Use updateTag() when a user needs to immediately see a change they just made.

React Compiler Is Stable, but You Don't Have to Enable It

React Compiler support became stable with Next.js 16.

React Compiler aims to reduce unnecessary renders by automatically memoizing components. However, it is not enabled by default.

To enable it:

const nextConfig = {
  reactCompiler: true,
}

export default nextConfig

You also need to install the compiler package:

npm install -D babel-plugin-react-compiler

I would follow the same principle here during migration:

First complete the Next.js 16 migration and verify application behavior. Then evaluate React Compiler as a separate optimization step.

React Compiler introduces additional work into the build pipeline, and development and production build times may increase.

What Should We Test After Migrating to Next.js 16?

A successful compile does not mean the migration is complete.

Before deploying to production, I would specifically verify the following:

  •  Are Node.js, TypeScript, React, and Next.js on supported versions?
  •  Do development and production builds work correctly with Turbopack?
  •  If custom Webpack configuration exists, has its Turbopack equivalent been reviewed?
  •  Have synchronous usages of params, searchParams, cookies(), headers(), and draftMode() been removed?
  •  Has the middleware.ts to proxy.ts migration been reviewed where applicable?
  •  Do authentication, redirects, rewrites, and locale routing still work?
  •  Have scripts and CI pipelines that rely on next lint been updated?
  •  Have next/image quality, caching, and local URL behaviors been tested?
  •  Do ISR, cache invalidation, and data updates after Server Actions behave as expected?
  •  Have dynamic routes and generateMetadata functions been tested?
  •  Have critical production pages been checked with Lighthouse and real user flows?

It is also much safer to perform the migration in a separate branch and compare the resulting Next.js 16 build against the existing Next.js 15 application instead of upgrading directly on the production branch.

How Should You Approach the Next.js 16 Migration?

Moving to a new major release should not be a goal on its own. For an actively developed Next.js application that is expected to remain in use for a long time, staying close to the current major version can provide important benefits in terms of security updates, framework improvements, and ecosystem support.

However, an unplanned migration can introduce unnecessary risk in large production applications that depend heavily on custom Webpack plugins, Edge Middleware, or older framework behavior.

For that reason, it is better to treat a major version migration as a small technical project rather than a routine dependency update.

At first glance, Next.js 16 stands out with features such as Turbopack, React Compiler, and Cache Components. But when migrating an existing application, the breaking changes deserve most of the attention.

Making Turbopack the default affects the build pipeline, async Request APIs affect App Router code, the move to proxy.ts changes the request layer, ESLint changes affect CI/CD workflows, and the new caching APIs can directly influence data update strategies.

The safest order to follow is simple:

Compatibility first, migration second, optimization last.

Start by updating dependencies and the runtime environment, then address the breaking changes and verify that the existing application behaves as expected. New features such as Turbopack, Cache Components, and React Compiler can then be adopted gradually after the migration itself is stable.

Because migrating to Next.js 16 and adopting every new feature in Next.js 16 at the same time are not the same thing.

With the codemods and upgrade tools provided by Next.js, moving from Next.js 15 to 16 does not have to be as painful as it may initially seem when the migration is handled in the right order.

If you want to build a scalable, high-performance web application with modern technologies or modernize the technical infrastructure of an existing project, Detartech can help you approach the project end to end with its web development solutions.

Have a project in mind?

Let's bring the technologies from this article to life in your project.

Request a Free Discovery Call