Next.js 15 already felt like a big shift with its move toward async APIs and the React 19 foundation. Next.js 16 finishes what 15 started. Turbopack is no longer opt-in. The caching model is finally predictable. Partial Prerendering blends static and dynamic rendering at the component level. And the framework leans harder into React 19 primitives, making Server Functions and the use() hook first-class citizens of the development experience.
Turbopack: now the default bundler
Turbopack has been in beta since Next.js 13. With version 16, it replaces webpack as the default bundler for both development and production builds. The difference is not subtle.
# Cold start on a large project
webpack: 12.4s
turbopack: 2.1s
HMR updates that used to take 800ms on a webpack build now land in under 50ms. The improvement scales with project size because Turbopack only recompiles the modules that changed, not the entire dependency graph. For large codebases with hundreds of routes, this turns the dev server from a bottleneck into something you forget is running.
The migration is transparent. Turbopack reads your next.config.ts the same way webpack did. Custom webpack configurations that rely on loaders or plugins need migration, but standard Next.js projects work without any changes.
The new caching model
Next.js 15 started undoing the aggressive caching defaults that confused developers. Next.js 16 completes the transition with a model that finally makes sense.
// Dynamic by default: no caching unless you opt in
async function ProductsPage() {
const products = await fetch('https://api.example.com/products');
return <ProductList products={await products.json()} />;
}
// Opt into caching explicitly
async function ProductsPage() {
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 },
});
return <ProductList products={await products.json()} />;
}
Fetch requests are no longer cached by default. GET route handlers are no longer cached by default. Client-side navigations no longer serve a stale prefetch cache for dynamic pages. Every caching behavior is now opt-in and explicit.
This sounds like a performance regression, but it’s the opposite. The old defaults led to stale data bugs that were incredibly hard to debug. Teams spent hours wondering why their dashboard showed yesterday’s numbers. The new model is predictable: if you want caching, you say so. If you don’t, data is always fresh.
Partial Prerendering: static and dynamic in one page
Partial Prerendering (PPR) is the feature that makes Next.js 16 architecturally different from everything else in the React ecosystem. A single page can have a static shell that ships instantly and dynamic holes that stream in from the server.
import { Suspense } from 'react';
export default function Dashboard() {
return (
<div>
{/* Static: rendered at build time */}
<h1>Dashboard</h1>
<Navigation />
{/* Dynamic: streamed at request time */}
<Suspense fallback={<StatsSkeleton />}>
<LiveStats />
</Suspense>
<Suspense fallback={<FeedSkeleton />}>
<ActivityFeed />
</Suspense>
</div>
);
}
The static parts (h1, Navigation) are prerendered into an HTML shell served from the CDN edge. The dynamic parts (LiveStats, ActivityFeed) are computed on request and streamed into the shell. The user sees the page layout instantly, and the data fills in progressively.
This is not the same as SSG + client-side fetching. The dynamic parts run on the server. They have access to the database, to secrets, to request headers. The browser receives rendered HTML, not JavaScript that needs to fetch data after hydration.
Async request APIs
Headers, cookies, params, and search params are now fully async. This change started in Next.js 15 as a deprecation and is now enforced.
// Before (Next.js 14): synchronous access
export default function Page({ params }: { params: { id: string } }) {
const id = params.id;
return <div>{id}</div>;
}
// After (Next.js 16): async access
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
return <div>{id}</div>;
}
The same applies to headers(), cookies(), and searchParams. They all return promises now. This enables the runtime to defer evaluation and optimize rendering order. It also means these values work naturally with Suspense boundaries and streaming.
The after() API: post-response work
after() lets you schedule work that runs after the response has been sent to the client. Analytics, logging, cache warming, anything that shouldn’t delay the user response.
import { after } from 'next/server';
export async function POST(request: Request) {
const order = await processOrder(request);
after(async () => {
await analytics.track('purchase', { orderId: order.id });
await emails.sendConfirmation(order);
await cache.invalidate(['user-orders', order.userId]);
});
return Response.json({ success: true, orderId: order.id });
}
The response returns immediately after processOrder. The analytics tracking, email sending, and cache invalidation happen in the background. No external queue, no separate worker, no added infrastructure. This pattern replaces a significant chunk of what teams used to spin up separate services for.
React 19 as the foundation
Next.js 16 fully embraces React 19 as its runtime. The React Compiler is integrated into the build pipeline. Server Functions (the evolution of Server Actions) work without the "use server" directive at the top of dedicated files, they can be defined inline.
export default async function PostsPage() {
const posts = await db.post.findMany();
async function deletePost(id: string) {
'use server';
await db.post.delete({ where: { id } });
revalidatePath('/posts');
}
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
{post.title}
<DeleteButton onDelete={() => deletePost(post.id)} />
</li>
))}
</ul>
);
}
The 'use server' directive marks individual functions, not files. The function closes over server-side variables (db, revalidatePath) and gets serialized as a reference for the client. Combined with the React Compiler eliminating manual memoization, components are cleaner and shorter than they’ve ever been.
Conclusion
Next.js 16 is the release where everything clicks. Turbopack makes the dev experience fast enough to forget about the toolchain. The caching model stops fighting developers and starts working with them. Partial Prerendering delivers on the promise of instant page loads without sacrificing dynamic data. For teams already on the App Router, this upgrade is straightforward and the payoff is immediate.