Modern Web · Nextjs

Next.js 15 New Features — A Complete Guide

What actually changed from Next.js 14 to 15: stable React 19 support, revised caching defaults for fetch and GET routes, the next/after API, a stable instrumentation.js, and faster Turbopack dev builds.

John Kihiu12 min read

Next.js 15's headline change isn't a new feature so much as a philosophy reversal: fetch requests and GET Route Handlers are no longer cached by default. That single change reshapes how much of the rest of the release you actually need to think about, because a lot of the "new" APIs exist to give you back the control that the old implicit caching used to paper over.

Caching defaults flipped from opt-out to opt-in

In Next.js 14, fetch() calls inside Server Components were cached indefinitely unless you explicitly opted out with { cache: 'no-store' }, which was a frequent source of "why is my data stale" bug reports from developers who didn't know caching was happening at all. Next.js 15 flips the default: fetch requests and GET Route Handlers are uncached unless you opt in with { cache: 'force-cache' } or a route segment config. The Full Route Cache and Router Cache still exist and still matter for performance, but you now have to ask for caching rather than remember to disable it — a much safer default for anyone who was quietly serving stale data in production without realizing it.

TSX · app/products/page.tsx
// Next.js 15: uncached by default, fresh data on every request
const res = await fetch('https://api.example.com/products');

// Opt in explicitly when you do want the old caching behavior
const cached = await fetch('https://api.example.com/products', {
  cache: 'force-cache',
});

Stable React 19 support

Next.js 15 ships with React 19 as the default, which brings the use() hook for reading promises and context conditionally, Actions as a first-class concept baked into React itself (not just a Next.js convention), and the new useActionState and useFormStatus hooks that used to live only in experimental or Next-specific APIs. Because Server Components and Server Actions were effectively a Next.js-led preview of ideas React later adopted, this release is where the framework and the underlying library's mental models finally converge — less translating between "how Next does it" and "how React does it."

next/after for post-response work

after() lets you schedule work — logging, analytics, sending a notification — to run after a response has already been streamed to the user, without making them wait for it. Before this existed, the workaround was firing off a non-awaited promise and hoping the serverless runtime didn't freeze the function before it finished, which was unreliable on platforms that suspend execution once the response closes. after() gives the runtime an explicit signal to keep the invocation alive just long enough to finish that trailing work.

TSX · app/checkout/actions.ts
'use server';
import { after } from 'next/server';
import { logOrderEvent } from '@/lib/analytics';

export async function completeOrder(orderId: string) {
  const order = await finalizeOrder(orderId);

  // Runs after the response is sent; doesn't delay the user
  after(() => {
    logOrderEvent('order_completed', { orderId: order.id });
  });

  return order;
}
after() is not a background job queue

It's scoped to the lifetime of the request on serverless platforms that support it — good for a log line or a webhook fire, not for long-running work. For anything that needs retries or durability, hand it to a real queue instead.

instrumentation.js goes stable

instrumentation.js, previously behind an experimental flag, is now a stable convention for registering observability tooling — OpenTelemetry exporters, error trackers — once when the server starts, rather than scattering setup code across every route. It exports a register() function that Next.js calls a single time at boot, which is the right place to wire up tracing before any request handling begins.

Turbopack dev improvements

next dev --turbo (now closer to being the default dev experience) continues closing the gap with webpack on compatibility while keeping its large advantage in cold start and incremental rebuild time on large codebases. It's still dev-only — Turbopack production builds were experimental at the 15.x line, not something to reach for in a Dockerfile yet — but for local iteration speed on a big App Router project, it's a noticeably different feel than webpack-based dev servers.

Wrapping up

If you're upgrading from 14, the caching default flip is the change that will actually break things if you don't audit for it — anywhere you were relying on implicit fetch caching now needs an explicit cache: 'force-cache' or a route segment config. Everything else in the release — React 19, after(), stable instrumentation, faster Turbopack dev — is additive and low-risk to adopt incrementally.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.