Next.js Upgrade Guide

Compare Next.js versions and see exactly what changes, what requires action, and what you can start using.

→
26Changes
14Breaking
25Actions
1Features
3Migration tools
Analyze your project

Check your package.json

UpgradePath detects relevant tools and selects them automatically.

Local only
Analyzed locally in your browser. Nothing is uploaded.
Project setup

What does your project use?

Select the tools in your project to include relevant dependency updates in this guide.

Breaking Changes

Request APIs become asynchronous

Action required
breakinghigh impact

cookies, headers, draftMode, params and searchParams require migration.

Beforetsx
import { cookies } from 'next/headers';
type Props = { params: { slug: string } };
export default function Page({ params }: Props) {
  const theme = cookies().get('theme')?.value;
  return <main data-theme={theme}>{params.slug}</main>;
}
Aftertsx
import { cookies } from 'next/headers';
type Props = { params: Promise<{ slug: string }> };
export default async function Page({ params }: Props) {
  const { slug } = await params;
  const store = await cookies();
  const theme = store.get('theme')?.value;
  return <main data-theme={theme}>{slug}</main>;
}
Migration

Await request APIs and route props in App Router code. Run npx @next/codemod@latest next-async-request-api . and review its output.

Node.js 20.9 and TypeScript 5.1 minimums

Action required
breakinghigh impact

Runtime and compiler minimum versions increase.

Migration

Update development, CI and deployment Node.js to at least 20.9; update TypeScript if used.

Synchronous request access is removed

Action required
breakinghigh impact

The temporary Next.js 15 compatibility path is gone.

Beforetypescript
// Next.js 15 temporary synchronous compatibility
import { cookies, type UnsafeUnwrappedCookies } from 'next/headers';
export function readTheme() {
  const store = cookies() as unknown as UnsafeUnwrappedCookies;
  return store.get('theme')?.value;
}
Aftertypescript
// Next.js 16: remove synchronous access
import { cookies } from 'next/headers';
export async function readTheme() {
  const store = await cookies();
  return store.get('theme')?.value;
}
Migration

Await cookies, headers, draftMode, params and searchParams throughout App Router code.

next lint is removed

Action required
breakinghigh impact

Builds no longer run lint automatically.

Beforejson
{
  "scripts": {
    "lint": "next lint",
    "build": "next build"
  }
}
Afterjson
{
  "scripts": {
    "lint": "eslint .",
    "build": "next build",
    "check": "npm run lint && npm run build"
  }
}
Migration

Run ESLint or Biome directly and add a separate lint check to CI.

Sitemap id becomes asynchronous

Action required
breakinghigh impact

The id passed to generated sitemap functions is a Promise.

Beforetypescript
import type { MetadataRoute } from 'next';
export async function generateSitemaps() {
  return [{ id: 0 }, { id: 1 }];
}
export default async function sitemap(
  { id }: { id: number },
): Promise<MetadataRoute.Sitemap> {
  return [{ url: 'https://example.com/catalog/' + id }];
}
Aftertypescript
import type { MetadataRoute } from 'next';
export async function generateSitemaps() {
  return [{ id: 0 }, { id: 1 }];
}
export default async function sitemap(
  { id }: { id: Promise<string> },
): Promise<MetadataRoute.Sitemap> {
  const shard = Number(await id);
  return [{ url: 'https://example.com/catalog/' + shard }];
}
Migration

Await id before using it; verify sitemap output and generated metadata images after upgrading.

Local image query strings need an allowlist

Action required
breakinghigh impact

Query strings on local next/image sources need an explicit localPatterns search pattern.

Beforejavascript
// next.config.mjs: local /assets/photo.jpg?v=2
export default {};
Afterjavascript
export default {
  images: {
    localPatterns: [{ pathname: '/assets/**', search: '?v=2' }],
  },
};
Migration

Allow only the paths and query strings your app uses; test optimized image URLs.

Image cache, sizes and quality defaults change

Action required
breakinghigh impact

minimumCacheTTL becomes 14400 seconds; 16px leaves imageSizes; qualities defaults to [75]. Unsupported direct optimizer quality requests can return 400.

Beforejavascript
// next.config.mjs: relying on Next.js 15 image defaults
export default {};
Afterjavascript
// Configure only the old behavior your application needs.
export default {
  images: {
    minimumCacheTTL: 60,
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    qualities: [60, 75, 90],
  },
};
Migration

Review image freshness and custom sizes/quality. Configure required values explicitly rather than restoring every old default.

Local IP image optimization is blocked

Action required
breakinghigh impact

The optimizer rejects private/local IP destinations by default.

Beforetsx
import Image from 'next/image';
// Private-IP source that may now be rejected.
export default function Photo() {
  return <Image src='http://10.0.0.5/photo.jpg' width={640} height={480} alt='Catalog' />;
}
Aftertsx
import Image from 'next/image';
// Prefer a trusted public origin, allowlisted with images.remotePatterns.
export default function Photo() {
  return <Image src='https://assets.example.com/photo.jpg' width={640} height={480} alt='Catalog' />;
}
// Do not enable dangerouslyAllowLocalIP as a blanket migration fix.
Migration

Prefer a trusted public image host. Enable dangerouslyAllowLocalIP only for a controlled private network after assessing SSRF exposure.

Image redirect chains are capped

Action required
breakinghigh impact

The optimizer follows at most three redirects by default.

Beforejavascript
// next.config.mjs: image redirects were previously unlimited
export default {};
Afterjavascript
// Prefer final asset URLs. If a longer chain is required:
export default {
  images: { maximumRedirects: 5 },
};
Migration

Use the final asset URL or set a justified finite maximumRedirects value.

AMP APIs and configuration are removed

Action required
breakinghigh impact

AMP pages, next/amp and AMP configuration no longer have framework support.

Beforetsx
// pages/article.tsx
import { useAmp } from 'next/amp';
export const config = { amp: 'hybrid' };
export default function Article() {
  const isAmp = useAmp();
  return <main><h1>Article</h1><p>{isAmp ? 'AMP edition' : 'Web edition'}</p></main>;
}
Aftertsx
// pages/article.tsx: standard page, no AMP APIs/configuration
export default function Article() {
  return <main><h1>Article</h1><p>Web edition</p></main>;
}
// Replace AMP-specific elements and review former AMP URLs/canonicals.
Migration

Remove AMP APIs and rework AMP-only markup as standard pages; review canonical URLs and redirects.

Parallel route slots require explicit defaults

Action required
breakinghigh impact

Missing default.js/default.tsx files in parallel slots can fail the build.

Beforetsx
// app/@modal/page.tsx exists, but @modal has no default.tsx.
export default function ModalPage() {
  return null;
}
Aftertsx
// Add app/@modal/default.tsx for unmatched direct navigation.
export default function ModalDefault() {
  return null;
}
// Add an appropriate default for each parallel slot;
// use notFound() if unmatched routes should return 404.
Migration

Add a fallback for every slot, returning null or calling notFound() as appropriate; test direct navigation and reloads.

Experimental PPR and cache flags are removed

Action required
breakinghigh impact

experimental.ppr, dynamicIO, useCache and route experimental_ppr require review; Cache Components uses a different rendering model.

Beforejavascript
// Experimental PPR on Next.js 15 canaries
export default { experimental: { ppr: true } };
// Routes may also export experimental_ppr.
Afterjavascript
// Only when intentionally migrating to Cache Components:
export default { cacheComponents: true };
// Remove experimental_ppr and migrate affected routes.
// Review Suspense boundaries and caching; this is not just a flag rename.
// If not adopting this model, remove old flags instead of enabling it.
Migration

Remove unused experimental flags. For active PPR adoption, follow the Cache Components migration with Suspense and cache boundaries; do not treat cacheComponents as a simple flag rename.

Runtime config APIs are removed

Action required
breakinghigh impact

serverRuntimeConfig, publicRuntimeConfig and next/config consumers need migration.

Beforetypescript
import 'server-only';
import getConfig from 'next/config';
export function readApiToken() {
  return getConfig().serverRuntimeConfig.apiToken;
}
Aftertypescript
import 'server-only';
import { connection } from 'next/server';
export async function readApiToken() {
  await connection(); // read this value at request time
  return process.env.API_TOKEN;
}
// Never prefix secrets with NEXT_PUBLIC_.
Migration

Use server-only environment variables for secrets. Use NEXT_PUBLIC_ only for public build-time values; use connection() when server values must be evaluated at request time.

Generated metadata image props become asynchronous

Action required
breakinghigh impact

Dynamic Open Graph/icon image functions receive asynchronous params and id; generation metadata callbacks have different contracts.

Beforetsx
import { ImageResponse } from 'next/og';
type Props = { params: { slug: string }; id: string };
export default function Image({ params, id }: Props) {
  return new ImageResponse(<div>{params.slug}: {id}</div>);
}
Aftertsx
import { ImageResponse } from 'next/og';
type Props = { params: Promise<{ slug: string }>; id: Promise<string> };
export default async function Image({ params, id }: Props) {
  const { slug } = await params;
  const imageId = await id;
  return new ImageResponse(<div>{slug}: {imageId}</div>);
}
// This is the image function, not generateImageMetadata.
Migration

Await params and id in the image function and test generated social/icon URLs; do not blindly change generateImageMetadata.

New Features

React Compiler configuration is stable and optional

featurelow impact

reactCompiler is a stable top-level option; it is not enabled by default.

Beforejavascript
// next.config.mjs: projects already using React Compiler
export default { experimental: { reactCompiler: true } };
Afterjavascript
// next.config.mjs: optional, not required for the upgrade
export default { reactCompiler: true };
// Keep babel-plugin-react-compiler installed if opting in.
Migration

If already using the compiler, move the option out of experimental and keep babel-plugin-react-compiler installed. Otherwise no enablement is required.

Tooling

Turbopack is the default for dev and build

Action required
toolingmedium impact

Custom webpack configuration needs review.

Beforejson
{
  "scripts": {
    "dev": "next dev",
    "build": "next build"
  }
}
Afterjson
{
  "scripts": {
    "dev": "next dev --webpack",
    "build": "next build --webpack"
  }
}
Migration

Migrate bundler configuration or use next dev --webpack and next build --webpack.

Next.js lint configuration adopts flat config

Action required
toolingmedium impact

The Next ESLint stack uses flat configuration; eslint-config-next 16 requires ESLint 9.

Beforejson
{"extends":["next/core-web-vitals","next/typescript"]}
Afterjavascript
// eslint.config.mjs (JavaScript, not JSON)
import { defineConfig } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTypes from "eslint-config-next/typescript";
export default defineConfig([...nextVitals, ...nextTypes]);
Migration

Migrate legacy config if adopting eslint-config-next 16. Keep compatible lint versions aligned and run lint separately from build.

Migration Tools

App Router adopts React 19

Action required
migrationmedium impact

React dependencies and types need coordinated updates.

Beforejson
{
  "dependencies": {
    "next": "^14.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}
Afterjson
{
  "dependencies": {
    "next": "^15.0.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}
Migration

Upgrade react and react-dom together. For TypeScript, update their types and check third-party peer dependencies.

App Router includes React 19.2

Action required
migrationmedium impact

Next.js 16 App Router includes React 19.2 capabilities; React and React DOM must stay aligned.

Beforejson
{
  "dependencies": {
    "next": "^15.0.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}
Afterjson
{
  "dependencies": {
    "next": "^16.0.0",
    "react": "^19.2.0",
    "react-dom": "^19.2.0"
  }
}
Migration

Install compatible patched React 19.2 releases and matching types; review peer dependencies.

cacheLife and cacheTag drop unstable prefixes

Action required
migrationmedium impact

The cache lifetime/tag helper names are stable.

Beforetypescript
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
Aftertypescript
import { cacheLife, cacheTag } from 'next/cache';
Migration

Replace unstable_cacheLife and unstable_cacheTag imports when adopting the supported caching model.

Behavior Changes

fetch is uncached by default

Action required
behaviormedium impact

Server fetch calls no longer opt into caching automatically.

Beforetypescript
// Next.js 14: cache-eligible server request
const response = await fetch('https://example.com/api/catalog');
Aftertypescript
// Next.js 15: opt in if this catalog should remain cached
const response = await fetch('https://example.com/api/catalog', {
  cache: 'force-cache',
});
Migration

Use cache: 'force-cache' explicitly where caching is intended; check freshness and SSR performance.

GET Route Handlers are uncached by default

Action required
behaviormedium impact

GET responses no longer default to cached output.

Beforetypescript
// app/api/catalog/route.ts
export async function GET() {
  return Response.json({ categories: ['books', 'music'] });
}
Aftertypescript
// Only for public data intended to be statically cached.
export const dynamic = 'force-static';
export async function GET() {
  return Response.json({ categories: ['books', 'music'] });
}
Migration

Opt eligible handlers into static caching with export const dynamic = 'force-static'.

Page segments are not cached by default

Action required
behaviormedium impact

Client navigation changes page cache reuse.

Beforejavascript
// next.config.mjs
// Next.js 14 page cache defaults
export default {};
Afterjavascript
// Optional: reuse dynamic page segments for 30 seconds.
export default {
  experimental: {
    staleTimes: { dynamic: 30 },
  },
};
Migration

Review navigation and staleTimes configuration; test back/forward behavior.

Deprecations

middleware is renamed to proxy

Action required
deprecationmedium impact

The middleware convention is deprecated.

Beforetypescript
// middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname === '/old-guide') {
    return NextResponse.redirect(new URL('/guides', request.url));
  }
  return NextResponse.next();
}
Aftertypescript
// proxy.ts: verify Node.js runtime compatibility
import { NextResponse, type NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
  if (request.nextUrl.pathname === '/old-guide') {
    return NextResponse.redirect(new URL('/guides', request.url));
  }
  return NextResponse.next();
}
Migration

For Node.js-compatible middleware, run npx @next/codemod@latest middleware-to-proxy .; proxy does not support Edge runtime. Keep middleware if Edge is required.

revalidateTag requires explicit cache semantics

Action required
deprecationmedium impact

The single-argument signature is deprecated.

Beforetypescript
'use server';
import { revalidateTag } from 'next/cache';
export async function invalidateCatalog() {
  revalidateTag('catalog');
}
Aftertypescript
'use server';
import { revalidateTag, updateTag } from 'next/cache';
export async function invalidateInBackground() {
  revalidateTag('catalog', 'max');
}
// Immediate read-your-writes in a Server Action:
export async function invalidateImmediately() {
  updateTag('catalog');
}
Migration

Use revalidateTag(tag, 'max') for stale-while-revalidate, or updateTag in Server Actions for immediate read-your-writes.

next/legacy/image is deprecated

Action required
deprecationmedium impact

Legacy image consumers need review before adopting the current component.

Beforetsx
import Image from 'next/legacy/image';
export default function Photo() {
  return <Image src='/photo.jpg' width={640} height={480} alt='Catalog' />;
}
Aftertsx
import Image from 'next/image';
export default function Photo() {
  return <Image src='/photo.jpg' width={640} height={480} alt='Catalog' />;
}
// Recheck layout/styling; the new component's behavior differs.
Migration

Move to next/image and test sizing, styling and loader behavior.