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.

Where each Next.js 16 feature lives in a request: build with Turbopack, request through proxy.ts, render dynamic-by-default with use cache, React Compiler and React 19.2, then mutate with updateTag, refresh, and revalidateTag

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:

Cache invalidation decision guide: use updateTag for read-your-writes inside a Server Action, revalidateTag with a cacheLife profile for shared content with stale-while-revalidate, and refresh for uncached data shown elsewhere

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 [email protected].

Get the next one in your inbox -> subscribe at astraedus.dev.