Next.js 16 didn't add a pile of new APIs. It changed the defaults you never think about: how your app builds, whether a page is cached, and which file intercepts a request. If you're upgrading from 15, five of those changes show up in your day, and two things I wanted still aren't here.
I run Next.js 16 in production on a small app (16.2 with Neon and Clerk), so this is the upgrade as it actually lands, not the changelog.
1. Turbopack is the default bundler
Turbopack is stable now and the default for every new project. You get 2 to 5x faster production builds and up to 10x faster Fast Refresh, with no config change. That's the single most noticeable difference on day one. On my own app (16.2), a clean production build lands in about 19 seconds, 8.3 of them Turbopack compiling, and I never touched the bundler config.
If you've got a custom webpack setup that Turbopack can't handle yet, opt out per command:
next dev --webpack
next build --webpack
Turbopack also caches compiler artifacts to disk between runs. For next dev that filesystem cache is on by default since 16.1, so your second dev start is faster for free. The same cache for next build is still experimental, and you opt in (more on that below):
// next.config.ts
const nextConfig = {
experimental: {
turbopackFileSystemCacheForBuild: true, // experimental
},
};
export default nextConfig;
2. Caching is opt-in now with "use cache"
This is the biggest mental shift, and the one that breaks the most assumptions. In Next.js 16, every page, layout, and route handler runs at request time by default. Nothing's silently cached. You opt a page or component into the cache with the "use cache" directive, and the compiler generates the cache key for you.
Turn it on in config first:
// next.config.ts
const nextConfig = {
cacheComponents: true,
};
export default nextConfig;
Then mark what you actually want cached:
// app/products/page.tsx
export default async function ProductsPage() {
'use cache';
const products = await db.products.findMany();
return <ProductGrid products={products} />;
}
The old implicit caching is gone. So are the experimental.ppr and experimental.dynamicIO flags, which the new Cache Components model replaces. If you upgrade a 15 app and your data suddenly looks fresher (and your database busier), that's why. Pages that used to be static now hit the database on every request, until you add "use cache" back where you meant it.
3. middleware.ts is proxy.ts now
This is the one that caught me on upgrade. middleware.ts is deprecated in favor of proxy.ts. The rename itself is mechanical, and a codemod does it for you:
npx @next/codemod@canary middleware-to-proxy
It renames the file and the exported function:
// proxy.ts (was middleware.ts)
import { NextRequest, NextResponse } from 'next/server';
export default function proxy(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url));
}
The part that isn't mechanical is the runtime. proxy.ts runs on the Node.js runtime, and it's meant to stay a thin proxy that clarifies your network boundary. If your old middleware leaned on Edge-runtime behavior, that's the bit to check, not the rename. My Clerk auth check moved over fine once I renamed the file and the function, but I would not have found that out from the changelog. middleware.ts still works for Edge cases, but it's deprecated and gets removed later.
4. New cache invalidation: updateTag() and refresh()
Once caching is explicit, invalidation has to be too. Next.js 16 splits it into three intents, and picking the wrong one is how you ship a stale UI. Here's the decision I make every time:
updateTag() is new and Server-Actions-only. It expires the cache and reads fresh data in the same request, so the user sees their own write immediately:
'use server';
import { updateTag } from 'next/cache';
export async function saveProfile(userId: string, profile: Profile) {
await db.users.update(userId, profile);
updateTag(`user-${userId}`); // read-your-writes
}
revalidateTag() now requires a cache profile as its second argument, which enables stale-while-revalidate. The single-argument form is deprecated:
revalidateTag('blog-posts', 'max'); // serve cached now, revalidate in background
And refresh() re-fetches uncached data shown elsewhere on the page (a notification count, a live metric) without touching the cache at all. Three intents, three functions, no guessing.
5. The React Compiler is stable
Built-in React Compiler support is stable now, following the compiler's 1.0 release. It automatically memoizes components and cuts unnecessary re-renders with zero code changes. It's not on by default, because it relies on Babel and can slow your build, so you enable it deliberately:
// next.config.ts
const nextConfig = {
reactCompiler: true,
};
export default nextConfig;
You also inherit React 19.2 in the App Router: View Transitions for animating navigations, useEffectEvent for non-reactive Effect logic, and <Activity> for keeping background UI mounted with its state intact.
The 2 I'm still waiting for
Filesystem build caching that's stable, not experimental. The dev filesystem cache is on by default now, and it's genuinely good. The build cache (turbopackFileSystemCacheForBuild) is still experimental, and that's the one I want most. A cold CI runner rebuilding from scratch on every push is exactly where a persistent compiler cache pays off. It's also exactly where I don't want to lean on an experimental flag yet.
One caching mental model instead of four functions. Cache Components is the right direction. But shipping it means holding "use cache", updateTag(), revalidateTag(tag, profile), and refresh() in your head at once, plus a cacheLife profile system on top. It's powerful. It's also a lot of surface area for "cache this and invalidate it correctly." I want the diagram above to eventually be one function.
The takeaway
If you upgrade one 15 app to 16 this week, budget for exactly two things. Rename middleware.ts to proxy.ts (and check the runtime, not just the filename), then go find every place that relied on implicit caching and add "use cache" back. Turbopack, the React Compiler, and the new invalidation APIs are upside you mostly get for free. The dynamic-by-default flip is the one that'll surprise you in production if you skip it. Run the codemod, read your build logs, and cache on purpose.
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)