Next.js 15 Is Here: What Actually Changed?
Next.js 15 represents one of the most consequential architectural transitions in the React ecosystem. Rather than introducing experimental gimmicks, this release stabilizes core performance foundations that teams have anticipated for over a year: Turbopack as the default dev bundler, React 19 support by default, Partial Pre-Rendering (PPR) for hybrid static/dynamic delivery, and a complete overhaul of request-time caching defaults.
For engineering teams maintaining production applications or building modern web applications, Next.js 15 requires deliberate architectural adjustments - most notably around async request APIs and uncached fetch calls.
In this guide, we break down every major feature, provide concrete migration code patterns, explore performance benchmarks, and walk through real-world production gotchas.
1. Turbopack Is Stable for Development
After extensive community testing across thousands of repositories, Turbopack is officially stable and enabled by default for local development via next dev.
Performance Gains in Practice
According to official benchmarks and real-world monorepo testing:
- Up to 76.7% faster local server startup compared to Webpack.
- Up to 96.3% faster Fast Refresh (HMR) on large enterprise codebases with thousands of modules.
- Substantially reduced idle memory footprint during sustained development sessions.
You no longer need to pass --turbopack in your package.json scripts:
# Automatically boots with Turbopack in Next.js 15
pnpm dev
# Build output confirms:
# ▲ Next.js 15.x.x (Turbopack)
# - Local: http://localhost:3000
# ✓ Starting...
# ✓ Ready in 380ms
Handling Custom Webpack Plugins
If your project relies on legacy custom Webpack loaders (such as specific SVG transformers, raw GLSL shaders, or custom Webpack hooks), Turbopack will alert you. You can temporarily fall back to Webpack using:
next dev --turbopack=false
However, most modern workflows - including Tailwind CSS, PostCSS, SVGR, and path aliases - work out of the box with zero configuration.
2. React 19 Integration & New Component Primitives
Next.js 15 adopts React 19 as its core rendering engine. This brings several long-awaited React primitives into standard App Router workflows:
Stable Server Actions & useActionState
Server Actions are now fully stabilized. The legacy useFormState hook from react-dom has been replaced by React 19's native useActionState, which natively manages pending states and server response payloads without extra state hooks:
// app/actions.ts
"use server";
import { z } from "zod";
const TaskSchema = z.object({
title: z.string().min(3, "Title must have at least 3 characters"),
});
export type ActionState = {
success?: boolean;
errors?: { title?: string[] };
};
export async function createTaskAction(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const result = TaskSchema.safeParse({
title: formData.get("title"),
});
if (!result.success) {
return { errors: result.error.flatten().fieldErrors };
}
// Persist to database
return { success: true };
}
// app/components/TaskForm.tsx
"use client";
import { useActionState } from "react";
import { createTaskAction, ActionState } from "@/app/actions";
const initialState: ActionState = {};
export function TaskForm() {
const [state, formAction, isPending] = useActionState(createTaskAction, initialState);
return (
<form action={formAction} className="space-y-4 max-w-md">
<input
type="text"
name="title"
placeholder="Enter task title"
className="w-full px-3 py-2 border rounded"
disabled={isPending}
/>
{state.errors?.title && (
<p className="text-red-500 text-sm">{state.errors.title[0]}</p>
)}
<button
type="submit"
disabled={isPending}
className="px-4 py-2 bg-slate-900 text-white rounded disabled:opacity-50"
>
{isPending ? "Saving..." : "Create Task"}
</button>
</form>
);
}
Reading Promises with the use() Hook
The React 19 use() API allows client components to read promises directly in the render phase when coupled with Suspense boundaries, eliminating boilerplate useEffect data-fetching patterns:
"use client";
import { use, Suspense } from "react";
function UserProfile({ userPromise }: { userPromise: Promise<{ name: string; email: string }> }) {
const user = use(userPromise);
return (
<div className="p-4 border rounded">
<h3 className="font-bold">{user.name}</h3>
<p className="text-sm text-gray-600">{user.email}</p>
</div>
);
}
export function UserSection({ userPromise }: { userPromise: Promise<{ name: string; email: string }> }) {
return (
<Suspense fallback={<p>Loading user profile...</p>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
3. The Critical Breaking Change: Async Request APIs
The most widespread breaking change in Next.js 15 is that synchronous request data is now asynchronous.
In Next.js 14 and earlier, request-specific context like cookies(), headers(), and page route params / searchParams were read synchronously. In Next.js 15, to prepare the runtime for upcoming server optimizations like streaming and PPR, these APIs return Promises.
Before vs After: Route Parameters (params)
In Next.js 14:
// Next.js 14 (DEPRECATED)
interface PageProps {
params: { slug: string };
searchParams: { page?: string };
}
export default function BlogPostPage({ params, searchParams }: PageProps) {
const slug = params.slug; // Synchronous access
const page = searchParams.page;
return <div>Post: {slug}</div>;
}
In Next.js 15:
// Next.js 15 (REQUIRED)
interface PageProps {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export default async function BlogPostPage({ params, searchParams }: PageProps) {
const { slug } = await params; // Must await params
const { page } = await searchParams; // Must await searchParams
return <div>Post: {slug} (Page: {page})</div>;
}
Before vs After: cookies() and headers()
// Next.js 14:
import { cookies, headers } from "next/headers";
export async function ServerComponent() {
const cookieStore = cookies();
const token = cookieStore.get("session_token");
}
// Next.js 15:
import { cookies, headers } from "next/headers";
export async function ServerComponent() {
const cookieStore = await cookies(); // Must be awaited
const headerList = await headers(); // Must be awaited
const token = cookieStore.get("session_token");
}
Pro Tip: Vercel provides an automated codemod to rewrite these signatures across your codebase:
bashnpx @next/codemod@canary next-async-request-api .
4. Caching Overhaul: fetch Is No Longer Cached by Default
In Next.js 13 and 14, one of the most controversial features was aggressive automatic caching on all fetch() requests (force-cache was the default). Developers often struggled with stale data during production deployments.
Next.js 15 reverses this behavior:
fetch()requests are now uncached by default (cache: "no-store").GETRoute Handlers are now uncached by default.- Client navigation caching (Router Cache) page staling time is now
0 secondsby default for dynamic pages.
// In Next.js 14: This was cached by default indefinitely.
// In Next.js 15: This executes dynamically on every request.
const res = await fetch("https://api.example.com/products");
// If you WANT caching in Next.js 15, you must be explicit:
const cachedRes = await fetch("https://api.example.com/products", {
cache: "force-cache",
next: { revalidate: 3600 }, // Revalidate every hour
});
For database queries and ORMs (Prisma, Drizzle, Mongoose) that do not use fetch, continue using unstable_cache to achieve granular ISR caching.
5. Partial Pre-Rendering (PPR)
Partial Pre-Rendering (PPR) combines static and dynamic rendering in a single HTTP response.
Traditionally, you had to choose between SSG (fast static HTML at edge CDN, but no personalized user data) or SSR (dynamic data on every request, but slower initial TTFB). With PPR:
- Next.js serves a static shell instantly from the nearest edge cache.
- Dynamic holes wrapped in React
<Suspense>stream in asynchronously over the same open connection.
To enable experimental PPR in next.config.ts:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
ppr: "incremental", // Allows opting in per-page or layout
},
};
export default nextConfig;
Then, on any page or layout, export the PPR flag:
// app/dashboard/page.tsx
import { Suspense } from "react";
import { UserGreeting } from "./UserGreeting";
import { StaticSidebar } from "./StaticSidebar";
export const experimental_ppr = true;
export default function Dashboard() {
return (
<div className="flex min-h-screen">
{/* Static Shell: Served immediately from CDN */}
<StaticSidebar />
<main className="flex-1 p-8">
<h1 className="text-2xl font-bold">Dashboard</h1>
{/* Dynamic Hole: Streams in as soon as user DB query finishes */}
<Suspense fallback={<div className="h-20 bg-gray-100 animate-pulse rounded" />}>
<UserGreeting />
</Suspense>
</main>
</div>
);
}
6. Background Work with the after() API
A frequent architectural challenge in serverless environments is performing secondary tasks - such as analytics logging, audit trails, and cache warming - without blocking the client response.
Next.js 15 introduces after(), which executes tasks after the response has been closed and flushed to the user:
import { after } from "next/server";
import { logAnalyticsEvent } from "@/lib/analytics";
export async function POST(request: Request) {
const body = await request.json();
// 1. Process critical user action
const order = await processOrder(body);
// 2. Schedule non-blocking background telemetry
after(async () => {
await logAnalyticsEvent({
event: "order_created",
orderId: order.id,
timestamp: Date.now(),
});
});
// 3. Client receives response immediately
return Response.json({ success: true, orderId: order.id });
}
This pattern dramatically improves Interaction to Next Paint (INP) and user-perceived latency.
7. Migration Checklist: Moving from Next.js 14 to 15
If you are upgrading an existing production application, follow this systematic upgrade sequence:
Step 1: Upgrade Dependencies
pnpm add next@latest react@latest react-dom@latest
pnpm add -D @types/react@latest @types/react-dom@latest eslint-config-next@latest
Step 2: Run the Official Codemod
npx @next/codemod@canary upgrade latest
Step 3: Audit Async Parameters
Search for all route handlers, layouts, and page components that use params, searchParams, cookies(), or headers(). Ensure they are marked async and awaited.
Step 4: Verify Fetch Caching Requirements
Review external API calls. If your application relies on cached responses (e.g. headless CMS posts, exchange rates, site settings), add { cache: "force-cache" } or next: { revalidate: seconds }.
Step 5: Check React 18 Compatibility Flags
If an older third-party package has strict peerDependencies specifying React 18:
# In pnpm (.npmrc or command line):
pnpm install --no-frozen-lockfile
Next.js 15 in Enterprise Architecture
Next.js 15 solidifies the App Router as an enterprise-grade standard for performant web systems. By pairing Turbopack with React 19 and Partial Pre-Rendering, applications achieve both lightning-fast development iterations and sub-second real-world page loads.
If you are designing high-performance digital platforms or modernizing legacy infrastructure, explore our work in building scalable enterprise web applications and browse our latest engineering insights across the Web Development category and our shipped case studies.

Comments
Have a question or something to add? Sign in to join the discussion.