Next.js 16 shipped on October 21, 2025, and it's the largest performance-focused release since the App Router launched. Build times dropped by 50% or more with the now-stable Turbopack bundler, and the new caching model gives you explicit control over what gets cached.
Next.js 16 is the October 2025 major release of the React framework. It makes Turbopack the default bundler (2-5x faster builds), introduces Cache Components with "use cache" directives replacing PPR, and bundles React 19.2. The upgrade from Next.js 15 is straightforward because most breaking changes are handled by codemods.
What you get in Next.js 16:
- Turbopack is stable and enabled by default (2-5x faster builds, up to 10x faster Fast Refresh)
- Cache Components replace PPR with explicit
"use cache"directives - New caching APIs:
updateTag(),refresh(), and an improvedrevalidateTag() proxy.tsreplaces middleware for clearer network boundary control- React 19.2 with
<Activity>,useEffectEvent, and View Transitions - React Compiler is now stable (still opt-in)
- DevTools MCP for AI-assisted debugging
Tested with Next.js 16.1 and React 19.2. Makerkit's Next.js SaaS kits ship with these defaults.
Should You Upgrade to Next.js 16?
Upgrade now if:
- You're starting a new project (Next.js 16 is the new default)
- Build times are a bottleneck for your team
- You want explicit control over caching with Cache Components
- You're already on Next.js 15 and have async dynamic APIs
Wait if:
- You depend on OpenNext and can't work around the
proxy.tslimitation - Your project uses AMP (all AMP APIs are removed)
- Third-party libraries you depend on aren't React 19.2 compatible yet
If unsure: Upgrade a staging environment first and run your test suite. Most projects migrate without issues.
Turbopack Is Now Stable and Default
Turbopack, the Rust-based bundler that replaces Webpack, graduates from experimental to stable in Next.js 16. It's enabled by default for both development and production builds.
The performance gains are substantial. In our testing with the Makerkit SaaS Kit:
- Development startup: 603ms (down from 1083ms in Next.js 15)
- Production builds: 5.7 seconds with Turbopack vs 24.5 seconds with Webpack
- Fast Refresh: Under 100ms for most changes
The Makerkit SaaS Kit now starts in 603ms with Next.js 16, nearly half the time of Next.js 15.
Filesystem Caching (Experimental)
Turbopack now supports filesystem caching, which persists compilation results across dev server restarts. This makes subsequent startups significantly faster.
Enable it in your next.config.ts:
next.config.ts
const nextConfig = { experimental: { turbopackFileSystemCacheForDev: true, turbopackFileSystemCacheForBuild: true, },};export default nextConfig;This is still experimental. We've seen occasional stale cache issues, so clear .next if you hit strange behavior.
Benchmarks: Next.js 16 vs Next.js 15
We benchmarked Next.js 16 against Next.js 15 using the Makerkit SaaS Kit on a MacBook Pro M3 Max with 36GB RAM.
Startup Time
Every metric improved. Edge instrumentation dropped from 38ms to 4ms. Total ready time dropped 44%.
Navigation Performance
Most routes are faster. The dynamic /home/[account] route shows a slight regression, which we're investigating. Initial page load dropped from 2.9s to 1.8s.
Production Build Times
Turbopack in Next.js 16 builds the Makerkit SaaS Kit in 5.7 seconds, which is 4.3x faster than Webpack on Next.js 16 and 4.9x faster than Webpack on Next.js 15.
Not quite Vite-level instant, but the gap is closing. For a real SaaS codebase with auth, billing, and database integrations, these numbers are solid.
Cache Components Replace PPR
The experimental ppr flag and dynamicIO are gone. Cache Components are the new model, and they're more explicit about what gets cached.
Instead of configuring PPR globally, you mark individual components or functions with "use cache":
app/posts/page.tsx
import { Suspense } from 'react';async function BlogPosts() { 'use cache'; const posts = await db.posts.findMany(); return <PostList posts={posts} />;}export default function PostsPage() { return ( <Suspense fallback={<PostsSkeleton />}> <BlogPosts /> </Suspense> );}The compiler generates cache keys automatically. Dynamic code runs at request time by default. Only "use cache" blocks get cached.
Enable Cache Components in your config:
next.config.ts
const nextConfig = { cacheComponents: true,};export default nextConfig;This approach is cleaner than PPR's incremental opt-in. You see exactly what's cached by looking at the code.
New Caching APIs
Next.js 16 introduces three caching functions that give you precise control over cache invalidation.
updateTag(): Immediate Cache Refresh
updateTag() expires a cache tag and immediately refreshes the data within the same request. This provides read-your-writes semantics: the user sees their change immediately, not stale data.
app/actions/profile.ts
'use server';import { updateTag } from 'next/cache';import { db } from '@/lib/db';export async function updateUserProfile(userId: string, data: ProfileData) { await db.users.update(userId, data); updateTag(`user-${userId}`);}Use updateTag() when the user needs to see their change immediately, like updating a profile or posting a comment.
Constraint: updateTag() only works in Server Actions. It can't be called from Route Handlers or anywhere else. Use revalidateTag() in those contexts.
revalidateTag(): Stale-While-Revalidate
revalidateTag() now requires a cache profile as the second argument. This enables stale-while-revalidate behavior: users see cached content immediately while Next.js refreshes in the background.
app/actions/posts.ts
'use server';import { revalidateTag } from 'next/cache';export async function publishPost(postId: string) { await db.posts.publish(postId); // Use built-in profiles revalidateTag('blog-posts', 'max'); // Or specify inline revalidateTag('homepage', { expire: 3600 });}Built-in profiles: 'max', 'hours', 'days'. Use revalidateTag() for content where brief staleness is acceptable, like blog posts or product listings.
refresh(): Update Uncached Data
refresh() updates uncached data without touching the cache at all. Use it for real-time indicators that shouldn't be cached.
app/actions/notifications.ts
'use server';import { refresh } from 'next/cache';export async function markNotificationRead(id: string) { await db.notifications.markRead(id); refresh();}This refreshes notification counts, unread indicators, or live metrics without invalidating your cached page shell.
When to Use Each
| Function | Use Case | Behavior |
|---|---|---|
updateTag() | User edits their data | Blocks until fresh data arrives |
revalidateTag() | Content updates | Shows stale data, refreshes in background |
refresh() | Real-time indicators | Updates uncached data only |
proxy.ts Replaces middleware.ts
The middleware filename is deprecated. Rename it to proxy.ts to clarify that it handles network boundary concerns like rewrites, redirects, and request modification.
Before (Next.js 15):
middleware.ts
import { NextResponse } from 'next/server';import type { NextRequest } from 'next/server';export function middleware(request: NextRequest) { if (!request.cookies.get('session')) { return NextResponse.redirect(new URL('/login', request.url)); } return NextResponse.next();}After (Next.js 16):
proxy.ts
import { NextResponse } from 'next/server';import type { NextRequest } from 'next/server';export function proxy(request: NextRequest) { if (!request.cookies.get('session')) { return NextResponse.redirect(new URL('/login', request.url)); } return NextResponse.next();}export const config = { matcher: ['/dashboard/:path*', '/settings/:path*'],};Key changes:
- Rename
middleware.tstoproxy.ts - Rename the exported function from
middlewaretoproxy(or use default export) - No more response bodies: Proxy can only rewrite, redirect, or modify headers. Return full responses from Route Handlers instead.
The middleware.ts file still works but is deprecated and will be removed in a future version. Run the codemod to migrate automatically:
npx @next/codemod@canary upgrade latestThe codemod will output: Migrated middleware.ts to proxy.ts and list the updated files.
If you use OpenNext: Check compatibility before migrating. As of this writing, OpenNext doesn't support proxy.ts yet. Keep using middleware.ts until support is added.
React 19.2
Next.js 16 bundles React 19.2 with several new features.
Activity Component
<Activity> controls component visibility while preserving state. Unlike conditional rendering, hidden components keep their state intact.
import { Activity } from 'react';function TabPanel({ activeTab }: { activeTab: string }) { return ( <> <Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}> <HomePage /> </Activity> <Activity mode={activeTab === 'settings' ? 'visible' : 'hidden'}> <SettingsPage /> </Activity> </> );}Use <Activity> when you want to pre-render content the user will likely visit, or preserve form state when switching tabs.
useEffectEvent Hook
useEffectEvent creates stable callbacks that always access the latest props and state without adding them to the dependency array.
import { useEffect, useEffectEvent } from 'react';function ChatRoom({ roomId, onMessage }) { const onMessageEvent = useEffectEvent((message) => { onMessage(roomId, message); }); useEffect(() => { const connection = connect(roomId); connection.on('message', onMessageEvent); return () => connection.disconnect(); }, [roomId]); // onMessage not needed in deps}This reduces unnecessary effect re-runs and makes dependency arrays honest.
View Transitions
React 19.2 supports the View Transitions API for animating navigation changes. Combined with Next.js's routing, you get smooth page transitions with minimal code.
For a detailed breakdown of React 19.2 changes including the new ESLint rules, see our React 19.2 upgrade guide.
React Compiler Is Stable
The React Compiler is now stable and moved outside the experimental phase. It automatically memoizes components and hooks, reducing unnecessary re-renders.
next.config.ts
const nextConfig = { reactCompiler: true,};export default nextConfig;Makerkit is fully compatible with the React Compiler. However, we keep it disabled by default because:
- Most Makerkit pages are server-rendered, so memoization provides limited benefit
- It can introduce performance regressions in some cases
- Third-party libraries may not be compatible
Enable it if your app has complex, client-heavy components that re-render frequently. Profile before and after to verify improvements.
Improved Navigation and Prefetching
Next.js 16 rewrote the prefetch cache with two significant optimizations.
Layout Deduplication
Shared layouts are now downloaded once and reused across prefetched links. If ten links share a layout, you download it once instead of ten times.
In our testing, this reduced prefetch data transfer by 60-80% on pages with many navigation links.
Incremental Prefetching
Next.js now only prefetches the parts of a route that aren't already cached. If you have the layout cached, it only fetches the page content.
Combined with automatic prefetch cancellation when links leave the viewport, navigation feels significantly smoother.
DevTools MCP for AI-Assisted Debugging
Next.js 16 introduces a Model Context Protocol (MCP) integration that helps AI coding assistants understand your application.
The DevTools MCP provides:
- Next.js routing, caching, and rendering knowledge
- Unified browser and server logs
- Automatic error access with stack traces
- Page and route awareness
If you use Claude Code, Cursor, or other AI coding tools, the MCP gives them context about your Next.js app structure, making debugging suggestions more accurate.
Common Pitfalls and How to Fix Them
Stale Turbopack filesystem cache
Symptom: Strange behavior after updating dependencies or changing config.
Fix: Clear the .next directory:
rm -rf .next && npm run devupdateTag() not working in Route Handlers
Symptom: Cache isn't invalidating when you call updateTag() from a Route Handler.
Fix: updateTag() only works in Server Actions. Use revalidateTag() in Route Handlers instead.
revalidateTag() missing profile argument
Symptom: TypeScript error or runtime error when calling revalidateTag().
Fix: Add the required cache profile as the second argument:
// Before (Next.js 15)revalidateTag('posts');// After (Next.js 16)revalidateTag('posts', 'max');proxy.ts not supported by your hosting provider
Symptom: Deployment fails or middleware doesn't run.
Fix: If you're using OpenNext or a provider that doesn't support proxy.ts yet, keep using middleware.ts. It's deprecated but still works.
Dynamic route regressions
Symptom: Some dynamic routes are slower than in Next.js 15.
Fix: Profile the specific route. Check if you're using uncached data fetching where caching would help. We observed this with /home/[account] and are investigating.
React Compiler breaks third-party libraries
Symptom: Runtime errors or unexpected behavior after enabling React Compiler.
Fix: Disable React Compiler and identify the problematic library. You can exclude specific files from compilation or wait for library updates.
Breaking Changes to Watch For
Minimum Version Requirements
- Node.js 20.9+ (Node 18 is no longer supported)
- TypeScript 5.1+
- Modern browsers: Chrome 111+, Edge 111+, Firefox 111+, Safari 16.4+
Removed Features
- AMP support: All AMP APIs are removed
next lint: Use Biome or ESLint directly- Sync dynamic APIs:
cookies(),headers(),params, andsearchParamsmust be awaited
Behavior Changes
- Turbopack is the default bundler
- Image optimization cache TTL increased from 60s to 4 hours
- All parallel route slots require explicit
default.jsfiles
Run the codemod to handle most migrations automatically. See the official upgrade guide for detailed instructions:
npx @next/codemod@canary upgrade latestMigration Checklist
Update dependencies
Update the dependencies to the latest version:
npm install next@latest react@latest react-dom@latestRename middleware to proxy
Rename the middleware file to proxy.ts:
mv middleware.ts proxy.tsUpdate the export from middleware to proxy:
export function proxy(request: NextRequest) {Update async dynamic APIs
const cookieStore = await cookies();const headerStore = await headers();const { id } = await params;Check third-party compatibility
You should check if the third-party libraries you are using are compatible with Next.js 16:
- OpenNext doesn't support
proxy.tsyet - Some libraries may not work with React Compiler
Update revalidateTag calls
Add the required profile argument: revalidateTag('tag', 'max')
Test caching behavior
Cache Components work differently than PPR. Verify your caching strategy with the new "use cache" directive.
Profile performance
Run benchmarks before and after. Check for navigation regressions on dynamic routes.
Conclusion
Next.js 16 delivers the performance improvements the community has been waiting for. Turbopack is stable and fast enough for production use, Cache Components provide explicit caching control, and the new caching APIs give you precise invalidation options.
The migration from Next.js 15 is straightforward. Most changes are handled by codemods, and the breaking changes are well-documented.
Our Next.js SaaS Starter Kit ships with Next.js 16 by default. If you're building a new SaaS, you get these performance improvements out of the box.
Frequently Asked Questions
Is Turbopack stable enough for production?
What happened to PPR (Partial Pre-Rendering)?
Do I need to migrate from middleware.ts immediately?
Should I enable the React Compiler?
What's the difference between updateTag and revalidateTag?
Is Next.js 16 compatible with OpenNext?
Resources
- Next.js 16 Drizzle SaaS Starter Kit - Our production-ready Next.js SaaS Starter Kit with Drizzle ORM
- Next.js 16 Prisma SaaS Starter Kit - Our production-ready Next.js SaaS Starter Kit with Prisma ORM
- Next.js 16 Release Announcement - Official blog post with full changelog
- Next.js 16 Upgrade Guide - Step-by-step migration instructions
- React 19.2 Upgrade Guide - Detailed breakdown of React 19.2 changes
- Server Actions Guide - Complete guide to Server Actions patterns
- Secure Server Actions - Security best practices for Server Actions