Next.js 16 is the biggest architecture shift since the App Router: caching is now opt-in instead of implicit, middleware.ts became proxy.ts, and Turbopack is the default bundler for every app. If you upgrade for one reason, upgrade because you finally control when your data is cached instead of guessing.
I run Next.js in production, and the caching model in 15 was the part I fought most. Half my no-store and revalidate lines were cargo cult, added to make a stale page go away without really knowing why it was stale. Next.js 16 kills that guessing game. Here are the five features that shipped and are worth your time, plus two things I wanted that aren't here yet.
The diagram is the whole mental model: a request hits proxy.ts, the route renders dynamically by default, and you opt specific pieces into caching. Everything below is a piece of that path.
1. Cache Components make caching explicit with "use cache"
Caching in 16 is opt-in. Every page, layout, and API route runs at request time by default, and you cache the parts you choose with the "use cache" directive. That's the opposite of the App Router's old implicit caching, where you often couldn't tell why a page went stale.
In 15 you fought the cache by disabling it:
// Next 15: opt OUT of caching you didn't ask for
export const dynamic = 'force-dynamic';
const data = await fetch(url, { cache: 'no-store' });
In 16 you turn one config flag on and opt specific things IN:
// next.config.ts
const nextConfig = { cacheComponents: true };
export default nextConfig;
async function ProductList() {
"use cache";
const products = await db.products.findMany();
return <List items={products} />;
}
The compiler generates the cache key for you wherever "use cache" appears. Under the hood this completes Partial Prerendering: the cached parts become a static shell that streams instantly, and the uncached parts (a logged-in user's cart, a live price) render per request as dynamic holes. Note the old experimental.ppr and experimental.dynamicIO flags are gone, folded into this one cacheComponents option.
2. Turbopack is the default now, and it's finally stable
Turbopack ships as the default bundler in Next.js 16, stable for both dev and production. Vercel measures 2 to 5 times faster production builds and up to 10 times faster Fast Refresh, with zero config. If you have a custom webpack setup, opt out per command:
next dev --webpack
next build --webpack
The 16.3 release pushed this further. Persistent filesystem caching for next build is on by default, and Vercel reports build times dropping 2.3 to 5.5 times on their own sites when the cache is warm. Dev-server memory also drops sharply, up to 90 percent on large apps, because Turbopack now evicts its in-memory cache to disk instead of holding every visited route in RAM.
One CI caveat that will bite you: the cache lives in .next/cache, so your builds only get faster if you restore that directory between runs.
3. middleware.ts is now proxy.ts
middleware.ts was renamed to proxy.ts, and it runs on the Node.js runtime. The rename is about honesty: the file intercepts requests at your network boundary, so it should say so in its name.
Migration is mechanical. Rename the file, rename the exported function, keep your logic:
// proxy.ts
export default function proxy(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url));
}
middleware.ts still works for Edge runtime cases, but it's deprecated and will be removed later. Rename it now while it's a one-line change.
4. New cache-invalidation APIs give you read-your-writes
Next.js 16 adds two new Server Action APIs and changes a third, so you control exactly how fresh your data is. The one I reach for most is updateTag(), which expires a tag and reads fresh data in the same request:
'use server';
import { updateTag } from 'next/cache';
export async function saveProfile(userId: string, profile: Profile) {
await db.users.update(userId, profile);
updateTag(`user-${userId}`); // user sees their edit immediately
}
refresh() refreshes uncached data only, useful for a live counter after an action. And revalidateTag() now takes a cacheLife profile as a second argument for stale-while-revalidate behavior:
revalidateTag('blog-posts', 'max'); // serve stale, revalidate in the background
The single-argument revalidateTag('blog-posts') still runs but is deprecated. The rule I use: updateTag when a user must see their own write, revalidateTag when eventual consistency is fine.
5. React Compiler support is stable
The React Compiler integration is stable in 16, following the compiler's 1.0 release. Flip one flag and it auto-memoizes your components, which removes most hand-written useMemo and useCallback:
// next.config.ts
const nextConfig = { reactCompiler: true };
Stable, yes, but read the caveat at the end before you flip it on. It's not on by default, because it relies on Babel and that slows your build. If build time is why you're hesitating, 16.3 added an experimental Rust port of the compiler that ran 20 to 50 percent faster in Vercel's tests, behind experimental.turbopackRustReactCompiler.
Now the 2 things I'm still waiting for
A guided caching story. Cache Components solved the "why is this stale" problem, but handed me a new one: deciding what to cache is entirely manual. The compiler generates keys, it doesn't tell me which boundaries are safe to cache. I want a lint rule or a codemod that flags cacheable server components, the way the React Compiler flags un-memoizable code. Right now the model is powerful and the guidance is thin.
The Rust compiler, stable and on by default. The whole point of the React Compiler is to delete manual memoization. But the default path is Babel, which taxes every build, so most teams leave it off. The fast native compiler exists and is experimental. Until it's stable and default, the compiler stays a "someday" checkbox for a lot of real apps.
Before you upgrade: the gotchas
The codemod handles most of it, but these bit me or would have. Read this list first:
- Node.js 20.9+ required. Node 18 is no longer supported. Check your CI image.
-
Request APIs are async.
params,searchParams,cookies(),headers(), anddraftMode()must be awaited. Thenext-async-request-apicodemod covers most call sites. -
Restore
.next/cachein CI. Miss this and every build is a cold build, so you lose the Turbopack speedup entirely. -
next/imagedefaults changed.qualitiesnow defaults to[75]and local IP optimization is blocked. Audit any image that relied on the old defaults. -
next lintis gone. Run Biome or ESLint directly;next buildno longer lints.
The takeaway
Next.js 16 asks you to say what you mean. You opt into caching, you name your network boundary, you turn the compiler on yourself. That's more upfront work than 15, and it's the right trade, because the implicit magic was exactly what made the framework hard to reason about.
Concrete upgrade path if you're on 15:
npx @next/codemod@canary upgrade latest- Run the
next-async-request-apiandmiddleware-to-proxycodemods. - Restore
.next/cachein CI. - Set
cacheComponents: trueand add"use cache"to the routes you actually want cached. - Ship, then watch build times drop on every run after.
I write these from real work at astraedus.dev, where I build apps and tools. Building something, or stuck on something like this? Reach me at astraedus.dev or theagentthatcould@gmail.com.
Get the next one in your inbox → subscribe at astraedus.dev.

Top comments (0)