Next.js 15 New Features — Every Breaking Change and Upgrade Explained

Sanjeev SharmaSanjeev Sharma
7 min read

Advertisement

Introduction

Why This Matters

Next.js 15 is the most opinionated release in the framework's history. The team reversed the default caching behavior that confused developers for two major versions, stabilized Turbopack after years of beta, and landed full React 19 support including Partial Prerendering. If you are running a Next.js 14 app in production, this guide gives you exactly what changed, why it changed, and how to migrate safely.

Caching Is Now Opt-In — The Biggest Breaking Change

In Next.js 14, fetch() inside Server Components was cached by default. In Next.js 15, it is not. Every fetch is treated as a fresh request on every render unless you explicitly opt in to caching.

// Next.js 14 — cached by default (ISR behavior)
const data = await fetch('https://api.example.com/products')
 
// Next.js 15 — NOT cached (SSR behavior, fetches fresh every request)
const data = await fetch('https://api.example.com/products')
 
// To get the old caching behavior in Next.js 15, opt in explicitly:
const data = await fetch('https://api.example.com/products', {
  cache: 'force-cache',
})
 
// Incremental Static Regeneration — revalidate every 60 seconds:
const data = await fetch('https://api.example.com/products', {
  next: { revalidate: 60 },
})

The same change applies to Route Handlers — GET handlers are no longer cached by default either.

Async Request APIs — params and cookies Are Now Promises

cookies(), headers(), params, and searchParams are now asynchronous in Next.js 15. Code that reads them synchronously will throw.

// Next.js 14 — synchronous reads
import { cookies, headers } from 'next/headers'
 
export default function Page({ params }: { params: { id: string } }) {
  const id = params.id                   // sync
  const cookieStore = cookies()          // sync
  const token = cookieStore.get('token')
}
 
// Next.js 15 — everything must be awaited
import { cookies, headers } from 'next/headers'
 
export default async function Page({
  params,
  searchParams,
}: {
  params: Promise<{ id: string }>
  searchParams: Promise<{ sort: string }>
}) {
  const { id } = await params
  const { sort } = await searchParams
  const cookieStore = await cookies()
  const headersList = await headers()
  const token = cookieStore.get('token')
 
  return <div>ID: {id}, Sort: {sort}, Token: {token?.value}</div>
}

Run the official codemod to migrate the majority of your files automatically:

npx @next/codemod@canary upgrade latest

Turbopack Is Now Stable for Development

Turbopack, Next.js's Rust-based bundler, is stable for next dev in version 15. Enable it with a single flag:

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

Measured performance improvements over webpack:

MetricImprovement
Local server startupUp to 76% faster
Fast Refresh (HMR)Up to 96% faster
Initial page compileSignificantly faster

Turbopack is not yet stable for next build — production builds still use webpack in 15. That stability is planned for a future release.

React 19 Support

Next.js 15 fully supports React 19. The most impactful new APIs for Next.js developers:

// React 19: use() — read a Promise inside a component (suspends until resolved)
import { use } from 'react'
 
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise)
  return <div>{user.name}</div>
}
 
// React 19: useActionState — the replacement for useFormState
import { useActionState } from 'react'
 
function ContactForm() {
  const [state, action, isPending] = useActionState(submitContact, null)
 
  return (
    <form action={action}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button disabled={isPending}>
        {isPending ? 'Sending...' : 'Send'}
      </button>
      {state?.error && <p className="error">{state.error}</p>}
      {state?.success && <p className="success">Message sent!</p>}
    </form>
  )
}

Partial Prerendering (PPR) — Incremental Adoption

Partial Prerendering renders the static shell of a page from the CDN instantly, then streams in dynamic content. In Next.js 15, PPR supports incremental adoption — you can enable it per route without changing your entire app:

// next.config.ts
const nextConfig = {
  experimental: {
    ppr: 'incremental',
  },
}
// app/products/page.tsx
import { Suspense } from 'react'
 
// Opt this specific route into PPR
export const experimental_ppr = true
 
export default function ProductsPage() {
  return (
    <main>
      {/* Static — served from CDN at build time */}
      <h1>All Products</h1>
      <ProductFilters />
 
      {/* Dynamic — streams in after the shell */}
      <Suspense fallback={<ProductGridSkeleton />}>
        <ProductGrid />
      </Suspense>
    </main>
  )
}

The <h1> and <ProductFilters /> render at build time and are cached at the edge. <ProductGrid /> streams in with fresh data on every request.

Improved next/form Component

The built-in <Form> component from next/form now prefetches the target route on focus and navigates without a full page reload:

import Form from 'next/form'
 
export default function SearchPage() {
  return (
    <Form action="/results">
      <input name="q" placeholder="Search..." />
      <button type="submit">Search</button>
    </Form>
  )
}

On submit, the form navigates to /results?q=<value> via client-side navigation, preserving the React tree.

Instrumentation API — Stable

The instrumentation.ts file (in the project root) now runs reliably when the server starts:

// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { NodeSDK } = await import('@opentelemetry/sdk-node')
    const sdk = new NodeSDK({ /* OTel config */ })
    sdk.start()
  }
}
 
export async function onRequestError(
  err: Error,
  _request: Request,
  context: { routeType: 'render' | 'route' | 'action' }
) {
  // Called for every unhandled request-level error
  await Sentry.captureException(err, { extra: context })
}

Common Mistakes

Not awaiting params in dynamic routes. This is the most common breakage during migration. Every access to params.id must become const { id } = await params.

Assuming fetch caching still works. Pages that relied on implicit ISR behavior will now fetch on every request, potentially causing slower responses and higher API costs. Audit your fetch calls before upgrading.

Enabling Turbopack in production builds. Turbopack's next build support is not stable yet — keep your CI pipeline on the default webpack build.

Best Practices

  • Run npx @next/codemod@canary upgrade latest before manually migrating — it handles the async params and cookies changes for most files.
  • Add cache: 'force-cache' or next: { revalidate } explicitly to every fetch that was relying on implicit caching in Next.js 14.
  • Enable ppr: 'incremental' and add experimental_ppr = true to your most traffic-heavy routes first — measure the impact before rolling out globally.
  • Use useActionState from React 19 instead of the deprecated useFormState from react-dom.
  • Enable Turbopack in development immediately — the HMR speed improvement alone makes it worth the switch.

Key Takeaways

  • In Next.js 15, fetch() is not cached by default — add cache: 'force-cache' or next: { revalidate } to opt in to caching behavior.
  • params, searchParams, cookies(), and headers() are now Promises and must be awaited in async Server Components.
  • Turbopack is stable for next dev in version 15, delivering up to 96% faster Hot Module Replacement compared to webpack.
  • Partial Prerendering supports incremental adoption per route via the experimental_ppr = true export — no all-or-nothing switch.
  • React 19's useActionState replaces the deprecated useFormState for managing form submission state alongside Server Actions.
  • The next/form component enables client-side navigation for form submissions with route prefetching on focus.
  • The instrumentation.ts API is now stable and supports an onRequestError hook for centralized error reporting to Sentry or similar services.
  • Run npx @next/codemod@canary upgrade latest to automate the migration of async request API changes across your codebase.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading