What's New in Next.js 15

25. October, 2025 11 min read Develop

A Major Step Forward

Next.js 15 is one of the most consequential releases in the framework's history. It rethinks caching defaults, makes request APIs asynchronous, stabilizes Turbopack for development, and fully embraces React 19 — all while laying the groundwork for features like Partial Prerendering.

Released in October 2024 and refined through several point releases, Next.js 15 brings changes that affect nearly every application. Some of these are breaking changes that require code updates, while others are new capabilities that unlock better performance and developer experience. In this post, we’ll cover everything you need to know.

Turbopack: A New Era for Builds

The Rust-powered bundler Turbopack reached stable status for development in Next.js 15. Enable it with:

next dev --turbo

The performance improvements are substantial. On Vercel’s own codebase, Turbopack delivers:

  • Up to 76.7% faster local server startup
  • Up to 96.3% faster code updates with Fast Refresh
  • Up to 45.8% faster initial route compilation

By version 15.2, compile times improved another 57.6% with a 30% decrease in memory usage. These aren’t synthetic benchmarks — they’re measured on a large production application.

Unlike Vite’s native ESM approach where modules are served unbundled during development, Turbopack bundles lazily — it only processes what’s needed for the current request. This gives you the fast startup of unbundled tools with the reliability of bundled output that matches production behavior more closely.

Turbopack uses SWC under the hood for JavaScript and TypeScript transformation, and Lightning CSS for CSS processing. It supports webpack loaders through the turbopack.rules configuration, though webpack plugins are not compatible. For most projects, the transition is seamless — just add --turbo to your dev command and see the difference.

Async Request APIs

The most impactful breaking change in Next.js 15 is that request-dependent APIs are now asynchronous:

  • cookies() — must be awaited
  • headers() — must be awaited
  • draftMode() — must be awaited
  • params — now a Promise in layouts, pages, and route handlers
  • searchParams — now a Promise in pages

Before (Next.js 14)

import { cookies } from 'next/headers';

export default function Dashboard() {
  const cookieStore = cookies();
  const token = cookieStore.get('session');

  return <div>Dashboard for {token?.value}</div>;
}

After (Next.js 15)

import { cookies } from 'next/headers';

export default async function Dashboard() {
  const cookieStore = await cookies();
  const token = cookieStore.get('session');

  return <div>Dashboard for {token?.value}</div>;
}

The same applies to params and searchParams:

// Next.js 15
export default async function Page({
  params,
  searchParams,
}: {
  params: Promise<{ slug: string }>;
  searchParams: Promise<{ query: string }>;
}) {
  const { slug } = await params;
  const { query } = await searchParams;

  return <div>Slug: {slug}, Query: {query}</div>;
}

Why this change? It enables the server to prepare as much work as possible before a request arrives. If a component doesn’t depend on request-specific data, it doesn’t need to wait for it — this is the foundation for Partial Prerendering, where static and dynamic parts of a page can be served independently.

A codemod handles most of the migration automatically:

npx @next/codemod@canary next-async-request-api .

The synchronous versions still work temporarily but show deprecation warnings. They’ll be removed in the next major version.

Caching: Uncached by Default

Next.js 14’s aggressive caching defaults were one of the most common sources of confusion. Next.js 15 reverses this — nothing is cached by default.

Fetch Requests

Data returned from fetch() is no longer automatically stored in the Data Cache. In dynamic rendering, every fetch gets fresh data. To explicitly cache:

// Not cached (default in Next.js 15)
const data = await fetch('https://api.example.com/posts');

// Explicitly cached
const data = await fetch('https://api.example.com/posts', {
  cache: 'force-cache',
});

// Cached with revalidation
const data = await fetch('https://api.example.com/posts', {
  next: { revalidate: 3600 },
});

GET Route Handlers

GET route handlers are no longer cached by default either. In Next.js 14, a GET handler without dynamic functions would be statically rendered at build time. Now it’s always dynamic unless you explicitly opt in:

// Opt into static caching
export const dynamic = 'force-static';

export async function GET() {
  return Response.json({ data: 'cached' });
}

Client Router Cache

Page components are no longer cached on the client during navigation. Every navigation fetches the latest data from the server. This means users always see fresh content, which eliminates the common complaint of stale data after mutations.

Back/forward navigation still uses the cache for instant transitions, and loading.js remains cached for 5 minutes. If you preferred the old behavior:

// next.config.js
const nextConfig = {
  experimental: {
    staleTimes: {
      dynamic: 30, // cache pages for 30 seconds
    },
  },
};

This change is philosophically significant. Next.js now defaults to correctness over performance, which is the right trade-off for most applications. You can always add caching where it matters, but stale data bugs caused by unexpected caching are much harder to diagnose.

React 19 Support

Next.js 15 ships with full React 19 support. The App Router uses React 19 by default, while the Pages Router maintains backward compatibility with React 18.

This means you get access to all React 19 features we covered in our React 19 post:

  • Server Actions as first-class form handlers
  • useActionState for managing action state
  • useFormStatus for pending states
  • useOptimistic for optimistic updates
  • The use API for reading promises during render

React 19 also brings “sibling pre-warming” for Suspense boundaries, which pre-renders the next sibling while the current one is suspended, improving perceived loading performance.

The after() API

The after() API lets you schedule work to run after the response finishes streaming. This is perfect for tasks that shouldn’t block the response — logging, analytics, syncing with external systems:

import { after } from 'next/server';

export default function Page() {
  after(() => {
    // This runs after the response is sent
    analytics.track('page_view', { page: '/dashboard' });
  });

  return <Dashboard />;
}

Introduced as unstable_after in 15.0 and stabilized in 15.1, it works in Server Components, Server Actions, Route Handlers, and Middleware. Unlike approaches that use waitUntil or fire-and-forget promises, after() is a proper API that the framework manages — ensuring the work completes even if the client disconnects.

Enhanced <Form> Component

The new <Form> component from next/form extends the HTML <form> element with framework-level optimizations:

import Form from 'next/form';

export default function SearchForm() {
  return (
    <Form action="/search">
      <input name="query" placeholder="Search..." />
      <button type="submit">Search</button>
    </Form>
  );
}

Three key improvements over a plain <form>:

  • Prefetching: When the form enters the viewport, the target layout and loading UI are prefetched automatically.
  • Client-side navigation: On submission, shared layouts and client-side state are preserved — no full page reload.
  • Progressive enhancement: If JavaScript hasn’t loaded yet, the form works via standard full-page navigation.

This replaces the boilerplate of combining useRouter, onSubmit handlers, and manual prefetching for navigational forms like search.

Instrumentation API

The instrumentation API is now stable. Define a instrumentation.ts file at the root of your project to tap into the Next.js server lifecycle:

// instrumentation.ts
export async function register() {
  // Initialize monitoring, logging, or observability tools
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { NodeSDK } = await import('@opentelemetry/sdk-node');
    new NodeSDK().start();
  }
}

export async function onRequestError(
  err: Error,
  request: { path: string; method: string },
  context: { routerKind: string; routeType: string }
) {
  // Send errors to your error tracking service
  await errorTracker.captureException(err, {
    path: request.path,
    routeType: context.routeType,
  });
}

The onRequestError hook was designed in collaboration with Sentry and provides structured context about where errors occur — whether in a Server Component, Server Action, Route Handler, or Middleware.

Error Handling Improvements

Next.js 15 significantly improved the developer experience around errors across its point releases:

  • 15.0: Better hydration error messages with source code display and fix suggestions
  • 15.1: Source maps that hide third-party dependency frames, making stack traces actually readable. Terminal error formatting now matches the browser output.
  • 15.2: A completely redesigned error overlay UI using React’s captureOwnerStack to pinpoint the exact subcomponent responsible for an error. A new dev indicator shows the rendering mode, compilation status, and active errors.

These may seem like small improvements, but they dramatically reduce debugging time. Seeing a clean stack trace that points to your code rather than React internals is a significant quality-of-life improvement.

Streaming Metadata

In Next.js 14, generateMetadata had to complete before any UI was sent to the browser — it blocked the initial paint. Starting in 15.2, metadata is streamed:

  • Initial UI is sent immediately
  • Metadata (<title>, <meta> tags) is streamed into the <head> when ready
  • Search engine crawlers still receive complete metadata upfront (detected via user agent)

This means slow API calls in generateMetadata no longer delay the entire page render.

TypeScript Configuration

Next.js 15 supports next.config.ts natively:

import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  reactCompiler: true,
  turbopack: {
    rules: {
      '*.svg': {
        loaders: ['@svgr/webpack'],
        as: '*.js',
      },
    },
  },
};

export default nextConfig;

Full autocomplete and type safety for your configuration — no more guessing option names.

Server Actions Security

Next.js 15 improves the security model for Server Actions. Unused Server Actions are now eliminated during the build via dead code elimination, so they’re never exposed as public HTTP endpoints. Actions that are used receive secure, non-deterministic IDs that are regenerated between builds. This means you can’t guess action endpoint URLs, and actions from previous deployments can’t be replayed.

These security improvements happen automatically — no configuration needed. But they reinforce an important principle: always validate authentication and authorization inside your Server Actions, regardless of how they’re exposed.

Self-Hosting Improvements

For teams running Next.js on their own infrastructure rather than Vercel, 15 brings several improvements:

  • Cache-Control: Custom Cache-Control headers are no longer overridden by the framework, giving you full control over CDN behavior.
  • Stale-While-Revalidate: The expireTime for stale-while-revalidate is now configurable, replacing the previous hardcoded value of one year.
  • Image optimization: sharp is automatically installed when running next start, removing a common deployment gotcha.

These changes reflect Next.js’s ongoing commitment to being a viable self-hosted framework, not just a Vercel product.

Partial Prerendering (Experimental)

While not yet stable, Partial Prerendering (PPR) is the feature that many of Next.js 15’s changes are building toward. PPR allows a single page to be split into a static shell and dynamic holes:

import { Suspense } from 'react';

export default function Page() {
  return (
    <main>
      <h1>Dashboard</h1>           {/* Static - served from cache */}
      <Sidebar />                    {/* Static - served from cache */}
      <Suspense fallback={<Skeleton />}>
        <DynamicContent />           {/* Dynamic - rendered on request */}
      </Suspense>
    </main>
  );
}

The static parts are served instantly from the edge, while dynamic parts stream in as they become available. This combines the speed of static sites with the freshness of server-rendered pages. PPR is why the async request APIs matter — they let the framework distinguish between components that need request data and those that don’t.

Migration from Next.js 14

The upgrade path is well-supported with an automated CLI:

npx @next/codemod@canary upgrade latest

This tool updates dependencies, shows available codemods, and guides you through applying them. The key areas to review after running the migration:

  1. Async APIs: The codemod converts most usage automatically, but review any dynamic patterns it couldn’t handle.
  2. Caching behavior: Previously cached code now fetches fresh data by default. Add explicit cache: 'force-cache' or revalidate where needed.
  3. Config renames: serverComponentsExternalPackages becomes serverExternalPackages; bundlePagesExternals becomes bundlePagesRouterDependencies.
  4. Node.js version: Minimum is now 18.18.0.

Conclusion

Next.js 15 is a mature, well-considered release that addresses real developer pain points. The caching changes alone eliminate one of the most common sources of confusion in Next.js applications. Turbopack makes development feel instant. And the async request APIs, while requiring migration effort, set the foundation for Partial Prerendering — a feature that will fundamentally change how we think about page rendering.

The upgrade tooling is solid, and the breaking changes are manageable with the provided codemods. If you’re still on 14, now is a great time to make the jump.

‘Till next time!