Next.js 15 shipped in October 2024 with a collection of changes that break some existing patterns while delivering meaningful improvements to build performance, data fetching reliability, and server-side execution control. If you are running a Next.js 14 project, this guide covers everything you need to know before upgrading, including what breaks and what the migration path looks like.
React 19 Support
Next.js 15 is the first version to officially support React 19. The upgrade brings several capabilities that change how components are written and how data flows through the render tree.
React 19 introduces the new React Compiler (formerly React Forget), which automatically memoizes components without requiring manual use of memo, useCallback, or useMemo. The compiler analyses your component tree at build time and inserts memoization where it determines a performance gain exists — reducing the mental overhead of manual optimisation.
The use() hook is another significant addition, allowing you to read Promises and context directly inside render without wrapping logic in useEffect. This works well with React's Suspense model: a component calls use(dataPromise), suspends until the promise resolves, and then renders with the data — without any loading state management boilerplate.
Actions formalise a pattern for handling form submissions and mutations server-side, with useOptimistic and useFormStatus rounding out the ergonomics for optimistic UI updates during pending server operations.
To enable the React Compiler in Next.js 15, add experimental: { reactCompiler: true } to your next.config.js. The compiler remains experimental — components with non-standard patterns (manual memoization that conflicts with compiler assumptions, for example) may behave unexpectedly. Enable it on a per-component basis using the opt-in comment if you want to test incrementally before turning it on project-wide.
Turbopack — Stable for Development
Turbopack, the Rust-based bundler that Vercel has been building as a replacement for Webpack, reaches stable status for next dev in Next.js 15. Enable it by running next dev --turbopack.
The performance difference is substantial on larger applications. Local server startup is approximately 76% faster than the Webpack equivalent, and Hot Module Replacement (HMR) — the time between saving a file and seeing the change reflected in the browser — is up to 96% faster. For a large monorepo with hundreds of components, this can cut the feedback loop from several seconds to under 300 milliseconds.
Turbopack for next build (production builds) remains in beta. Most teams are running Turbopack in development and keeping Webpack for their CI/CD production build pipeline. This is a reasonable split — the development speed gains are real and stable, while the production build path still needs more time to reach full feature parity.
Migration is straightforward for most Next.js applications: add the --turbopack flag and your app will likely work without changes. The known friction points are custom Webpack loader configurations that do not yet have Turbopack equivalents. The Turbopack compatibility page on the Next.js docs lists which loaders are supported — check this before enabling if your project uses custom webpack config.
Caching Behaviour Changes — Breaking Change
This is the change that has caused the most friction for teams upgrading from Next.js 13 or 14. Understanding it clearly before upgrading will save debugging time.
In Next.js 13 and 14, fetch() calls inside Server Components were cached by default. A fetch request would be stored in the Next.js data cache and subsequent renders would use the cached response unless you explicitly opted out with { cache: 'no-store' }. The intent was performance, but the behaviour was surprising to developers who expected standard fetch semantics.
Next.js 15 reverses this default entirely. Fetch requests now behave like standard browser fetch — no caching by default, equivalent to cache: 'no-store'. To cache a request, you now explicitly set { cache: 'force-cache' } or use the revalidate option: { next: { revalidate: 3600 } }.
GET Route Handlers follow the same change — they are no longer cached by default in Next.js 15. If your API routes returned cached responses previously without explicit cache headers, they will now return fresh data on every request.
The impact on production applications can be significant. Applications relying on implicit fetch caching for performance will suddenly be hitting their upstream APIs or databases on every request. Before upgrading, audit your Server Components for fetch calls that lack explicit cache configuration and decide intentionally what the caching strategy should be.
The after() API — Post-Response Execution
after() is a new experimental API introduced in Next.js 15 that lets you schedule code to run after the response has been streamed to the client. This is useful for work that needs to happen in response to a page visit but should not delay the user from seeing the page.
Practical use cases: logging analytics events without blocking page response; sending Slack or email notifications triggered by a specific page load; writing audit logs to a secondary database after the primary response is complete.
The usage pattern is straightforward:
import { after } from 'next/server';
export default function Page() {
after(() => {
trackPageView();
});
return <PageContent />;
}
The callback passed to after() executes asynchronously on the server after the response has been sent. It does not block Time to First Byte — the page streams to the client immediately and the after callback runs independently. Enable it via experimental: { after: true } in next.config.js.
One important constraint: after() callbacks have a time limit and are not appropriate for long-running tasks. Treat them as fire-and-forget operations for lightweight side effects, not background job processing.
Partial Prerendering (PPR) — Incremental Adoption
Partial Prerendering was introduced as an experimental concept in Next.js 14. In Next.js 15, it moves toward a more stable, incrementally adoptable state that teams can start integrating into existing applications without rewriting page architecture.
PPR allows a single page to combine a statically prerendered shell with dynamically streamed content. The page's static shell — navigation, header, above-the-fold layout, anything that does not depend on request-time data — is rendered at build time and served instantly from CDN cache. Dynamic content, wrapped in <Suspense> boundaries, streams in asynchronously on each request.
This addresses a long-standing architectural trade-off in Next.js: a page is either fully static (fast, served from cache, but potentially stale) or fully dynamic (always fresh, but slower to serve). With PPR, the choice is made at the component level rather than the page level. A product listing page can serve its header and layout statically while streaming personalised recommendations dynamically.
Enable incremental PPR adoption with experimental: { ppr: 'incremental' } in next.config.js, then opt in specific layouts or pages using the export const experimental_ppr = true export. This lets you test PPR on lower-traffic pages before rolling it out across your application.
instrumentation.ts — Stable Observability Hook
The instrumentation.ts file (placed in your project root or src/ directory) runs when the Next.js server starts and is the designated location for initialising observability and monitoring tooling. In Next.js 15, this feature moves from experimental to stable.
The primary use case is SDK initialisation: OpenTelemetry tracers, Sentry's server-side SDK, Datadog APM agents, and similar tools need to be set up before any request is handled. The register() function in instrumentation.ts runs once per server instance — not on each request — making it the right place for one-time setup code.
Next.js 15 also adds the onRequestError() hook within the instrumentation context. This hook fires on unhandled server-side errors and gives you a structured way to forward error details to your monitoring service — separate from the error boundary system in React, which handles client-side rendering errors.
For teams already using Sentry with Next.js, the instrumentation.ts path is now the recommended approach over the older _app.tsx initialisation pattern, which did not reliably cover Server Component errors.
Form Component — Built-in Progressive Enhancement
Next.js 15 ships a <Form> component exported from next/form that extends the standard HTML <form> element with prefetching, client-side navigation, and streaming integration.
The behaviour works like this: on hover, the <Form> component prefetches its action route, so the destination page is already loading by the time the user submits. On submission, navigation happens client-side without a full page reload — the URL updates and the result renders with the same speed characteristics as a Link navigation. The form also works with Server Actions for full-stack form handling without requiring a separately defined API route.
The primary intended use case is forms where submission produces a new page or view — search forms, filter forms, and navigation forms. A search bar implemented as <Form action="/search"> will prefetch the /search route and submit the query as URL parameters with client-side navigation, providing noticeably faster UX than a standard HTML form doing a full page request.
For forms that submit data mutations (creating a record, updating a profile), Server Actions remain the appropriate pattern — the <Form> component is specifically optimised for the navigate-on-submit use case.
Upgrading from Next.js 14 to 15
Start with the automated codemod that the Next.js team ships: npx @next/codemod@canary upgrade latest. This updates your package.json dependencies, handles known breaking changes in next/image, and migrates runtime configuration patterns that changed between versions.
After running the codemod, several manual audit steps are necessary before the upgrade is safe to deploy:
Fetch caching audit: Search your codebase for fetch( calls in Server Components and Route Handlers. Any call without an explicit cache option that previously relied on Next.js 14's default caching will now be uncached. Add { cache: 'force-cache' } or { next: { revalidate: N } } where caching is required.
Async APIs: In Next.js 15, cookies(), headers(), and searchParams are now async — they return Promises that you must await. In Next.js 14 these were synchronous. The codemod handles many of these automatically, but review any custom usage manually.
Turbopack compatibility check: If your next.config.js contains custom Webpack configuration, review each custom loader and plugin against the Turbopack compatibility documentation before enabling --turbopack in development.
Route Handler caching: GET Route Handlers are no longer cached by default. If any of your GET handlers served cached responses, add explicit export const revalidate = N to restore the previous behaviour.
The recommended upgrade approach: run the migration on a staging environment, execute your full integration test suite, and monitor error rates and cache hit metrics carefully after deploying to production. The caching default change in particular can produce subtle production issues that only surface under real traffic patterns.
Frequently Asked Questions
Is Turbopack stable enough to use in production builds in Next.js 15?
Turbopack for next dev is stable in Next.js 15 and recommended for development. Turbopack for next build (production) is still in beta — Vercel recommends continuing to use Webpack for production builds until Turbopack build support reaches stable. The practical path: enable --turbopack for next dev to get the faster HMR and dev server startup; keep your CI/CD pipeline and next build on the default Webpack. You can track Turbopack's build feature parity at areweturboyet.com — the team updates this as features complete.
Does switching to Next.js 15's uncached fetch defaults break existing applications?
Yes — this is the most commonly reported breaking change when upgrading from Next.js 14. Applications that relied on implicit fetch caching (not using { cache: 'no-store' } or { next: { revalidate: ... } } explicitly) will now make uncached network requests on every page render. The symptom: increased latency, higher origin server load, or unexpected fresh data where cached data was expected. Fix: after upgrading, run your application and use Next.js's logging to identify which fetch calls are making unexpected requests, then add { cache: 'force-cache' } or { next: { revalidate: 3600 } } to restore the previous caching behaviour intentionally.
How does Partial Prerendering differ from Incremental Static Regeneration (ISR)?
ISR regenerates an entire page in the background on a schedule (or on-demand) and serves the cached version until regeneration completes — the whole page is either static or dynamic at any point. PPR operates at a sub-page level: the static shell of a page (everything outside <Suspense> boundaries) is always served from CDN cache instantly; the dynamic content within <Suspense> boundaries streams on every request. The practical difference: with ISR, a page showing a user's personalised dashboard would need to be fully dynamic (no ISR) or show stale personalised data (with ISR). With PPR, the page's nav, header, and footer serve statically from cache, while the personalised dashboard section streams dynamically — combining the speed of static with the freshness of dynamic.