Node.js 18.17 or newer is required
Action requiredOlder Node.js runtimes are unsupported.
Update local, CI and hosting runtimes before building.
Compare Next.js versions and see exactly what changes, what requires action, and what you can start using.
UpgradePath detects relevant tools and selects them automatically.
Select the tools in your project to include relevant dependency updates in this guide.
Older Node.js runtimes are unsupported.
Update local, CI and hosting runtimes before building.
Static export is configured through output.
// build: next build && next export
module.exports = {};// build: next build
module.exports = {
output: 'export',
};Set output: 'export' in next.config.js and run next build. Check dynamic routes and server-only features.
Use the built-in font module.
import { Roboto } from '@next/font/google';
const font = Roboto({ weight: '400', subsets: ['latin'] });import { Roboto } from 'next/font/google';
const font = Roboto({ weight: '400', subsets: ['latin'] });Replace @next/font imports with next/font; run npx @next/codemod@latest built-in-next-font .
The next/server import is replaced.
import { ImageResponse } from 'next/server';
export async function GET() {
return new ImageResponse(<div>UpgradePath</div>);
}import { ImageResponse } from 'next/og';
export async function GET() {
return new ImageResponse(<div>UpgradePath</div>);
}Import ImageResponse from next/og and verify generated social images.
cookies, headers, draftMode, params and searchParams require migration.
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>;
}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>;
}Await request APIs and route props in App Router code. Run npx @next/codemod@latest next-async-request-api . and review its output.
Runtime and compiler minimum versions increase.
Update development, CI and deployment Node.js to at least 20.9; update TypeScript if used.
The temporary Next.js 15 compatibility path is gone.
// 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;
}// Next.js 16: remove synchronous access
import { cookies } from 'next/headers';
export async function readTheme() {
const store = await cookies();
return store.get('theme')?.value;
}Await cookies, headers, draftMode, params and searchParams throughout App Router code.
Builds no longer run lint automatically.
{
"scripts": {
"lint": "next lint",
"build": "next build"
}
}{
"scripts": {
"lint": "eslint .",
"build": "next build",
"check": "npm run lint && npm run build"
}
}Run ESLint or Biome directly and add a separate lint check to CI.
The id passed to generated sitemap functions is a Promise.
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 }];
}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 }];
}Await id before using it; verify sitemap output and generated metadata images after upgrading.
Query strings on local next/image sources need an explicit localPatterns search pattern.
// next.config.mjs: local /assets/photo.jpg?v=2
export default {};export default {
images: {
localPatterns: [{ pathname: '/assets/**', search: '?v=2' }],
},
};Allow only the paths and query strings your app uses; test optimized image URLs.
minimumCacheTTL becomes 14400 seconds; 16px leaves imageSizes; qualities defaults to [75]. Unsupported direct optimizer quality requests can return 400.
// next.config.mjs: relying on Next.js 15 image defaults
export default {};// 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],
},
};Review image freshness and custom sizes/quality. Configure required values explicitly rather than restoring every old default.
The optimizer rejects private/local IP destinations by default.
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' />;
}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.Prefer a trusted public image host. Enable dangerouslyAllowLocalIP only for a controlled private network after assessing SSRF exposure.
The optimizer follows at most three redirects by default.
// next.config.mjs: image redirects were previously unlimited
export default {};// Prefer final asset URLs. If a longer chain is required:
export default {
images: { maximumRedirects: 5 },
};Use the final asset URL or set a justified finite maximumRedirects value.
AMP pages, next/amp and AMP configuration no longer have framework support.
// 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>;
}// 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.Remove AMP APIs and rework AMP-only markup as standard pages; review canonical URLs and redirects.
Missing default.js/default.tsx files in parallel slots can fail the build.
// app/@modal/page.tsx exists, but @modal has no default.tsx.
export default function ModalPage() {
return null;
}// 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.Add a fallback for every slot, returning null or calling notFound() as appropriate; test direct navigation and reloads.
experimental.ppr, dynamicIO, useCache and route experimental_ppr require review; Cache Components uses a different rendering model.
// Experimental PPR on Next.js 15 canaries
export default { experimental: { ppr: true } };
// Routes may also export experimental_ppr.// 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.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.
serverRuntimeConfig, publicRuntimeConfig and next/config consumers need migration.
import 'server-only';
import getConfig from 'next/config';
export function readApiToken() {
return getConfig().serverRuntimeConfig.apiToken;
}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_.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.
Dynamic Open Graph/icon image functions receive asynchronous params and id; generation metadata callbacks have different contracts.
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>);
}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.Await params and id in the image function and test generated social/icon URLs; do not blindly change generateImageMetadata.
reactCompiler is a stable top-level option; it is not enabled by default.
// next.config.mjs: projects already using React Compiler
export default { experimental: { reactCompiler: true } };// next.config.mjs: optional, not required for the upgrade
export default { reactCompiler: true };
// Keep babel-plugin-react-compiler installed if opting in.If already using the compiler, move the option out of experimental and keep babel-plugin-react-compiler installed. Otherwise no enablement is required.
Custom webpack configuration needs review.
{
"scripts": {
"dev": "next dev",
"build": "next build"
}
}{
"scripts": {
"dev": "next dev --webpack",
"build": "next build --webpack"
}
}Migrate bundler configuration or use next dev --webpack and next build --webpack.
The Next ESLint stack uses flat configuration; eslint-config-next 16 requires ESLint 9.
{"extends":["next/core-web-vitals","next/typescript"]}// 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]);Migrate legacy config if adopting eslint-config-next 16. Keep compatible lint versions aligned and run lint separately from build.
React dependencies and types need coordinated updates.
{
"dependencies": {
"next": "^14.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}{
"dependencies": {
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}Upgrade react and react-dom together. For TypeScript, update their types and check third-party peer dependencies.
Next.js 16 App Router includes React 19.2 capabilities; React and React DOM must stay aligned.
{
"dependencies": {
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}{
"dependencies": {
"next": "^16.0.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
}
}Install compatible patched React 19.2 releases and matching types; review peer dependencies.
The cache lifetime/tag helper names are stable.
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';import { cacheLife, cacheTag } from 'next/cache';Replace unstable_cacheLife and unstable_cacheTag imports when adopting the supported caching model.
Server fetch calls no longer opt into caching automatically.
// Next.js 14: cache-eligible server request
const response = await fetch('https://example.com/api/catalog');// Next.js 15: opt in if this catalog should remain cached
const response = await fetch('https://example.com/api/catalog', {
cache: 'force-cache',
});Use cache: 'force-cache' explicitly where caching is intended; check freshness and SSR performance.
GET responses no longer default to cached output.
// app/api/catalog/route.ts
export async function GET() {
return Response.json({ categories: ['books', 'music'] });
}// Only for public data intended to be statically cached.
export const dynamic = 'force-static';
export async function GET() {
return Response.json({ categories: ['books', 'music'] });
}Opt eligible handlers into static caching with export const dynamic = 'force-static'.
Client navigation changes page cache reuse.
// next.config.mjs
// Next.js 14 page cache defaults
export default {};// Optional: reuse dynamic page segments for 30 seconds.
export default {
experimental: {
staleTimes: { dynamic: 30 },
},
};Review navigation and staleTimes configuration; test back/forward behavior.
The middleware convention is deprecated.
// 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();
}// 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();
}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.
The single-argument signature is deprecated.
'use server';
import { revalidateTag } from 'next/cache';
export async function invalidateCatalog() {
revalidateTag('catalog');
}'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');
}Use revalidateTag(tag, 'max') for stale-while-revalidate, or updateTag in Server Actions for immediate read-your-writes.
Legacy image consumers need review before adopting the current component.
import Image from 'next/legacy/image';
export default function Photo() {
return <Image src='/photo.jpg' width={640} height={480} alt='Catalog' />;
}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.Move to next/image and test sizing, styling and loader behavior.