Christian B. Martinez Avatar PhotoChristian B. Martinez
Dec 14, 2025

What's New in Next.js 16: A Deep Dive

The Next.js team just dropped version 16, and honestly, it's packed with some game-changing improvements.

nextjsreactframeworks
Image showing a computer mouse pointer hovering over a security button in a browser.
Photo by Pixabay via Pexels

This isn't just another incremental update. We're talking major improvements to Turbopack performance, a complete rethinking of how caching works, and some architectural changes that will impact how you build Next.js apps. Let me walk you through the highlights.

The Big New Features

First up, here's what landed since the beta release:

  • Cache Components - A completely new approach to caching that leverages Partial Pre-Rendering and gives you explicit control over what gets cached
  • Next.js Devtools MCP - AI-powered debugging tools that understand your Next.js app's context
  • The Great Middleware Rename - middleware.ts becomes proxy.ts for better clarity
  • Developer Experience Wins - Way better logging so you can actually see where your build time goes

And if you've been following the beta releases, you already know about these features that are now stable:

  • Turbopack is now the default - Say goodbye to webpack (unless you need it) and hello to 5-10x faster Fast Refresh
  • File system caching for Turbopack - Your dev server restarts just got way faster
  • React Compiler support - Automatic memoization without lifting a finger
  • Build Adapters API - For deployment platforms to hook into the build process
  • Smarter routing - Layout deduplication and incremental prefetching make navigation feel instant
  • New caching APIs - updateTag() and an updated revalidateTag()
  • React 19.2 goodies - View Transitions, useEffectEvent(), and more

Ready to Upgrade?

The Next.js team built an automated upgrade tool that handles most of the heavy lifting. Here's how to get started:

terminal
# Use the automated upgrade CLI
npx @next/codemod@canary upgrade latest

# ...or upgrade manually
npm install next@latest react@latest react-dom@latest

# ...or start a new project
npx create-next-app@latest
# Use the automated upgrade CLI
npx @next/codemod@canary upgrade latest

# ...or upgrade manually
npm install next@latest react@latest react-dom@latest

# ...or start a new project
npx create-next-app@latest

The codemod is pretty smart, but for edge cases, you'll want to check out the full upgrade guide in the docs.

Let's Talk About Cache Components

This is probably the most significant change in Next.js 16. Cache Components fundamentally change how you think about caching in Next.js.

Here's the thing: previous versions of the App Router had implicit caching that, let's be honest, confused a lot of developers. You weren't always sure what was cached and what wasn't. Cache Components flip the script by making caching entirely opt-in through a new "use cache" directive.

Now, everything is dynamic by default. Pages, layouts, and API routes all execute at request time unless you explicitly cache them. This is a huge win for developer expectations - what you write is what runs, no surprises.

But here's where it gets really interesting: Cache Components complete the vision of Partial Pre-Rendering (PPR) that the team introduced back in 2023. Before PPR, you had to choose whether a route was static or dynamic - there was no middle ground. PPR let you mix static and dynamic content on the same page using Suspense boundaries, but now Cache Components give you fine-grained control over exactly what gets cached and for how long.

To enable it, just add this to your config:

next.config.ts
const nextConfig = {
cacheComponents: true,
};

export default nextConfig;
const nextConfig = {
cacheComponents: true,
};

export default nextConfig;

The team is planning to share more details at Next.js Conf, and I'm expecting deep dives in the docs soon. This is definitely a feature worth paying attention to.

Important note: The old experimental.ppr flag is gone. Cache Components is the new way forward, so if you were using PPR in beta, you'll need to migrate.

AI-Powered Debugging with MCP

Next.js 16 introduces something I didn't expect to see in a framework release: built-in AI debugging support through the Model Context Protocol (MCP).

What does this actually mean for you? Well, AI assistants can now:

  • Understand your Next.js app - They know about routing, caching, and rendering behaviors specific to Next.js
  • Access unified logs - No more switching between browser and server console tabs
  • Read your errors automatically - Stack traces get sent to your AI assistant without you copying and pasting
  • Know what page you're on - Context-aware debugging based on your current route

This isn't just about making debugging easier - it's about letting AI tools understand the full context of your Next.js application so they can give you better, more specific help. Pretty cool stuff.

Goodbye middleware.ts, Hello proxy.ts

Okay, this one might seem like just a rename, but there's a good reason behind it. The file formerly known as middleware.ts is now proxy.ts.

The name change makes it clearer what this file actually does: it sits at the network boundary of your app and intercepts requests. Plus, it now runs exclusively on the Node.js runtime, which means more predictable behavior.

Here's how the migration looks:

proxy.ts
export default function proxy(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url));
}
export default function proxy(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url));
}

Just rename your file from middleware.ts to proxy.ts, change the exported function name to proxy, and you're done. The logic stays exactly the same.

Heads up: You can still use middleware.ts for Edge runtime cases, but it's officially deprecated. Start planning your migration now.

Build Logs That Actually Help

If you've ever stared at a slow Next.js build wondering where all the time went, you'll appreciate this. The dev server and build logs now show you exactly where time is being spent.

Development requests now break down into:

  • Compile time - How long routing and compilation take
  • Render time - How long your code and React rendering take

And builds now show timing for each step in the process:

terminal
Next.js 16 (Turbopack)

Compiled successfully in 615ms
Finished TypeScript in 1114ms
Collecting page data in 208ms
Generating static pages in 239ms
Finalizing page optimization in 5ms
Next.js 16 (Turbopack)

Compiled successfully in 615ms
Finished TypeScript in 1114ms
Collecting page data in 208ms
Generating static pages in 239ms
Finalizing page optimization in 5ms

This might seem like a small thing, but trust me - when you're optimizing build times, knowing where the bottlenecks are is invaluable.


Turbopack: Now Stable and Default

Let's talk about the elephant in the room: Turbopack is now the default bundler for all Next.js projects. This has been in beta for a while, and the numbers are impressive.

More than 50% of Next.js 15.3+ dev sessions are already using it, and for good reason:

  • Production builds are 2-5× faster
  • Fast Refresh is up to 10× faster

For most developers, this is a free performance win. You don't need to configure anything - it just works. But if you have custom webpack configuration that you're not ready to migrate, you can opt back into webpack:

terminal
next dev --webpack
next build --webpack
next dev --webpack
next build --webpack

File System Caching Makes Dev Restarts Instant

One of my favorite beta features is now available: file system caching for Turbopack. If you work on a large codebase, you know the pain of waiting for the dev server to boot up after a restart.

With file system caching, Turbopack saves compiled artifacts to disk. When you restart your dev server, it picks up right where it left off. The difference is especially noticeable on large projects.

To enable it:

next.config.ts
const nextConfig = {
experimental: {
turbopackFileSystemCacheForDev: true,
},
};

export default nextConfig;
const nextConfig = {
experimental: {
turbopackFileSystemCacheForDev: true,
},
};

export default nextConfig;

Vercel's internal teams have been using this for a while now, and the productivity gains are real. Give it a try and let the team know how it works for you.

React Compiler Goes Stable

The React Compiler has been a hot topic in the React community, and Next.js 16 now has stable support for it.

If you're not familiar, the React Compiler automatically memoizes your components. No more manual useMemo, useCallback, or React.memo calls - the compiler handles optimization for you.

That said, it's not enabled by default yet. The team is still gathering performance data across different types of apps, and since it relies on Babel, you'll see longer compile times during development and builds.

To enable it:

next.config.ts
const nextConfig = {
reactCompiler: true,
};

export default nextConfig;
const nextConfig = {
reactCompiler: true,
};

export default nextConfig;

You'll also need to install the compiler plugin:

terminal
npm install babel-plugin-react-compiler@latest
npm install babel-plugin-react-compiler@latest

My take: If you're working on a performance-critical app with lots of re-renders, this is worth experimenting with. Just be aware of the trade-offs in build time.

Routing Gets Smarter

Next.js 16 completely overhauls how routing and navigation work under the hood. You won't need to change your code, but you'll definitely notice the performance improvements.

Layout Deduplication

Here's a common scenario: you have a product listing page with 50 links, all sharing the same layout. In previous versions, Next.js would prefetch that shared layout 50 times - once for each link.

Now? It downloads the layout exactly once and reuses it for all the links. This dramatically reduces network transfer sizes, especially on pages with lots of internal links.

Incremental Prefetching

The prefetch cache is also smarter now. Instead of downloading entire pages, Next.js only prefetches the parts that aren't already cached. Plus, it:

  • Cancels prefetch requests when links leave the viewport
  • Re-prioritizes prefetches when you hover over links
  • Automatically re-fetches when cached data becomes stale
  • Works seamlessly with Cache Components

There's a trade-off here: you might see more individual network requests in your DevTools, but the total amount of data transferred is much lower. For most apps, this is absolutely the right call.

New Caching APIs You Need to Know

Next.js 16 introduces some important changes to how you invalidate and refresh cached data. Let me break down the three APIs you'll be working with.

revalidateTag() - Now with SWR

The revalidateTag() function has a new required second parameter that enables stale-while-revalidate behavior:

example.ts
import { revalidateTag } from 'next/cache';

// ✅ Recommended: use the 'max' profile for most cases
revalidateTag('blog-posts', 'max');

// Or use other built-in profiles
revalidateTag('news-feed', 'hours');
revalidateTag('analytics', 'days');

// Or specify a custom expiration time
revalidateTag('products', { expire: 3600 });

// ⚠️ Deprecated - this will stop working
revalidateTag('blog-posts');
import { revalidateTag } from 'next/cache';

// ✅ Recommended: use the 'max' profile for most cases
revalidateTag('blog-posts', 'max');

// Or use other built-in profiles
revalidateTag('news-feed', 'hours');
revalidateTag('analytics', 'days');

// Or specify a custom expiration time
revalidateTag('products', { expire: 3600 });

// ⚠️ Deprecated - this will stop working
revalidateTag('blog-posts');

With SWR enabled, users get cached content immediately while Next.js revalidates in the background. This is perfect for content that changes occasionally but doesn't need to be instantly fresh.

The team recommends using 'max' for most use cases, which gives you the longest cache lifetime with background revalidation.

updateTag() - For Immediate Updates

Sometimes you need users to see their changes right away. That's where updateTag() comes in. It's Server Actions-only and provides "read-your-writes" semantics:

actions.ts
'use server';

import { updateTag } from 'next/cache';

export async function updateUserProfile(userId: string, profile: Profile) {
await db.users.update(userId, profile);

// Cache is immediately invalidated and refreshed
updateTag(`user-${userId}`);
}
'use server';

import { updateTag } from 'next/cache';

export async function updateUserProfile(userId: string, profile: Profile) {
await db.users.update(userId, profile);

// Cache is immediately invalidated and refreshed
updateTag(`user-${userId}`);
}

Use this for forms, user settings, and any interactive feature where users expect to see their changes instantly. Think profile updates, shopping cart modifications, or comment submissions.

refresh() - For Uncached Data

The new refresh() API is for refreshing uncached data only. It doesn't touch any cached content:

actions.ts
'use server';

import { refresh } from 'next/cache';

export async function markNotificationAsRead(notificationId: string) {
await db.notifications.markAsRead(notificationId);

// Refreshes dynamic data like notification counts
// Cached page shells stay fast
refresh();
}
'use server';

import { refresh } from 'next/cache';

export async function markNotificationAsRead(notificationId: string) {
await db.notifications.markAsRead(notificationId);

// Refreshes dynamic data like notification counts
// Cached page shells stay fast
refresh();
}

This is really useful for updating things like notification badges, live metrics, or status indicators without invalidating your entire page cache.

React 19.2 Features Built-In

Next.js 16's App Router uses the latest React Canary release, which includes the newly stable React 19.2 features. Here are the highlights:

  • View Transitions - Native browser transitions for smoother page and component updates
  • useEffectEvent - Finally, a clean way to extract non-reactive logic from Effects
  • <Activity /> - Hide UI with display: nonewhile preserving state

These aren't Next.js-specific features, but having them available in the framework means you can start using them in production apps today.

Breaking Changes and Migration Guide

Alright, let's talk about what might break when you upgrade. Next.js 16 has some significant breaking changes, so don't skip this section.

Minimum Version Requirements

First, check your environment:

RequirementMinimum Version
Node.js20.9.0 (Node 18 is no longer supported)
TypeScript5.1.0
BrowsersChrome 111+, Edge 111+, Firefox 111+, Safari 16.4+

What Got Removed

These features are completely gone:

Removed FeatureWhat to Use Instead
AMP supportAll AMP-related APIs are removed. If you need AMP, you'll need to stay on Next.js 15.
next lintUse ESLint or Biome directly. There's a codemod to help migrate.
Runtime configserverRuntimeConfig and publicRuntimeConfig are gone. Use .env files instead.
Synchronous paramsAll params and searchParams are now async. You must await them.
Synchronous server functionscookies(), headers(), and draftMode() must all be awaited now.

Default Behavior Changes

These things work differently now, even if you didn't change any code:

What ChangedThe Details
Default bundlerTurbopack is now the default. Add --webpack to your commands if you need to opt out.
Image optimization cacheminimumCacheTTL went from 60 seconds to 4 hours. This reduces costs but means images stay cached longer.
Image sizesThe 16 size was removed from defaults (only 4.2% of projects used it).
Image qualityNow defaults to 75 instead of allowing 1-100. Quality values are coerced to the closest value in images.qualities.

What's Deprecated

These still work but will be removed in a future version:

DeprecatedMigration Path
middleware.tsRename to proxy.ts
next/legacy/imageUse next/image
images.domainsUse images.remotePatterns
revalidateTag(tag)Use revalidateTag(tag, profile)

My Take: Should You Upgrade?

If you're starting a new project, absolutely use Next.js 16. The performance improvements alone make it a no-brainer, and Turbopack being stable means you're building on solid ground.

For existing projects, here's my advice: read through the breaking changes carefully. The async params and server functions change is the biggest one - it'll require code changes throughout your app. The automated codemod will help, but you'll likely need to do some manual cleanup.

The caching changes are powerful, but they do require you to think differently about how data flows through your app. Take some time to understand revalidateTag(), updateTag(), and refresh() - choosing the right one for each use case will make a big difference in user experience.

Join the Conversation

The Next.js team is actively looking for feedback on this release. If you run into issues or have thoughts on the new features, here's where to share:

Wrapping Up

Next.js 16 represents a major evolution in how the framework handles performance, caching, and developer experience. The move to make Turbopack the default, the introduction of Cache Components, and the new caching APIs all point toward a more explicit, predictable framework.

Yes, there are breaking changes. Yes, you'll need to update your code. But the performance gains and clearer mental model are worth it.

Huge shoutout to the Next.js team (Andrew, Hendrik, Janka, Jiachi, Jimmy, Jiwon, JJ, Josh, Jude, Sam, Sebastian, Sebbie, Wyatt, and Zack), the Turbopack team (Benjamin, Josh, Luke, Niklas, Tim, Tobias, and Will), and the docs team (Delba, Rich, Ismael, and Joseph) for putting this together. And of course, thanks to the 3,000+ contributors who've made Next.js what it is today.

Now go upgrade your apps and let me know what you think!

Enjoyed this post?

Let's build something together.

I'm currently partnering with teams to ship performant, design-forward web experiences. Reach out if you'd like to collaborate.