Published on

Next.js 15 - Every New Feature Explained

Authors

Introduction

Next.js 15 brought major improvements across the board — smarter caching, async APIs, full Turbopack stability, React 19 support, and powerful new security features.

This guide covers every significant change with code examples.

1. Caching Changes — Fetch No Longer Cached by Default

This is the biggest breaking change in Next.js 15.

In Next.js 14, fetch() was cached by default. In Next.js 15, it's not:

// Next.js 14 — cached by default (ISR behavior)
const data = await fetch('https://api.example.com/data')

// Next.js 15 — NOT cached by default (SSR behavior)
const data = await fetch('https://api.example.com/data')

// To opt INTO caching in Next.js 15:
const data = await fetch('https://api.example.com/data', {
  cache: 'force-cache'  // Explicit opt-in
})

// Or use revalidate for ISR:
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 3600 }  // Revalidate every hour
})

This change makes behavior more predictable and easier to reason about.

2. Async Request APIs

In Next.js 15, cookies(), headers(), and params are now async:

// Next.js 14 — synchronous
import { cookies, headers } from 'next/headers'

export default function Page() {
  const cookieStore = cookies()  // Was sync
  const headersList = headers()  // Was sync
  const token = cookieStore.get('token')
}

// Next.js 15 — async (breaking change!)
export default async function Page({
  params,
  searchParams,
}: {
  params: Promise<{ id: string }>
  searchParams: Promise<{ query: string }>
}) {
  const { id } = await params          // Must await!
  const { query } = await searchParams // Must await!

  const cookieStore = await cookies()  // Must await!
  const headersList = await headers()  // Must await!
  const token = cookieStore.get('token')

  return <div>ID: {id}, Query: {query}</div>
}

3. Turbopack Stable

Turbopack (Next.js's Rust-based bundler) is now stable for development in Next.js 15:

package.json
{
  "scripts": {
    "dev": "next dev --turbopack"  // Enable Turbopack
  }
}

Performance improvements:

  • Up to 76% faster local server startup
  • Up to 96% faster Fast Refresh
  • Significantly faster initial page compilation

Turbopack is now the recommended default for development.

4. React 19 Support

Next.js 15 fully supports React 19, which brings:

// React 19: use() hook — read promises and context
import { use } from 'react'

function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise)  // Suspends until resolved
  return <div>{user.name}</div>
}

// React 19: New form hooks
import { useActionState } from 'react'

function ContactForm() {
  const [state, action, isPending] = useActionState(submitForm, null)

  return (
    <form action={action}>
      <input name="message" />
      <button disabled={isPending}>
        {isPending ? 'Sending...' : 'Send'}
      </button>
      {state?.error && <p>{state.error}</p>}
    </form>
  )
}

5. Partial Prerendering (PPR) — Stable

Partial Prerendering renders the static shell instantly, then streams in dynamic content:

next.config.ts
const nextConfig = {
  experimental: {
    ppr: 'incremental',  // Enable PPR per route
  },
}
app/product/[id]/page.tsx
import { Suspense } from 'react'

// Enable PPR for this route
export const experimental_ppr = true

// Static shell renders instantly
export default function ProductPage({ params }) {
  return (
    <main>
      <h1>Our Products</h1>  {/* Static — renders at build time */}

      {/* Dynamic content — streams in after */}
      <Suspense fallback={<ProductSkeleton />}>
        <ProductDetails id={params.id} />
      </Suspense>

      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews id={params.id} />
      </Suspense>
    </main>
  )
}

The static shell is served from the CDN instantly. Dynamic parts stream in progressively. Best of both worlds!

6. New Security: allowedDevOrigins

next.config.ts
const nextConfig = {
  experimental: {
    allowedDevOrigins: [
      'http://localhost:3001',
      'https://my-preview.vercel.app',
    ],
  },
}

7. Improved next/form Component

The new <Form> component from next/form adds client-side navigation to forms:

app/search/page.tsx
import Form from 'next/form'

export default function SearchPage() {
  return (
    <Form action="/search">
      {/* Prefetches the /search page on focus */}
      {/* Navigation happens without full page reload */}
      <input name="query" placeholder="Search..." />
      <button type="submit">Search</button>
    </Form>
  )
}

8. Instrumentation API — Stable

The instrumentation.ts file runs code when your server starts — perfect for observability setup:

instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    // Set up OpenTelemetry, Sentry, etc.
    const { NodeSDK } = await import('@opentelemetry/sdk-node')
    const sdk = new NodeSDK({ /* config */ })
    sdk.start()
  }
}

export async function onRequestError(
  err: Error,
  request: Request,
  context: { routeType: 'render' | 'route' | 'action' }
) {
  // Called for every unhandled request error
  await Sentry.captureException(err)
}

Migrating from Next.js 14

The main things to update:

# Upgrade
npm install next@latest react@latest react-dom@latest

# Run the codemod for most breaking changes
npx @next/codemod@canary upgrade latest

Key migration checklist:

  • Await params, searchParams, cookies(), headers()
  • Update fetch() calls that relied on implicit caching
  • Test all dynamic routes with the new async params

Conclusion

Next.js 15 is a significant release that makes the framework more predictable (explicit caching), faster (Turbopack stable, PPR), and more powerful (React 19, instrumentation). The caching change is the most impactful for existing apps, but the codemod handles most of the migration automatically. Upgrade when ready — the improvements are well worth it.