← Back to Articles

Next.js 16 in Production: What Actually Changed and How to Upgrade

A practical look at Next.js 16 and React 19.2: Turbopack by default, explicit caching with Cache Components, proxy.ts, async request APIs, and the breaking changes that will bite you during an upgrade.

By Urban M.•
Next.jsReactWeb DevelopmentPerformance
Next.js 16 in Production: What Actually Changed and How to Upgrade

Next.js 16 in Production: What Actually Changed and How to Upgrade

When we wrote about Next.js 15, the story was speed: Turbopack, streaming, Server Components settling in. Next.js 16 is a different kind of release. It's less about new capabilities and more about making the framework's behavior explicit — especially caching, which has been the single most confusing part of the App Router since it launched.

This site runs on Next.js 16 with React 19.2, deployed as a standalone Docker image. Everything below is written from the perspective of a real upgrade, not a feature announcement: what changed, what broke, and what is genuinely worth adopting.


The Big Shift: Caching Is Now Opt-In

Previous App Router versions cached aggressively and implicitly. A fetch call might be cached; a page might be statically rendered or not depending on which APIs it touched. Many teams learned the caching rules by debugging stale data in production.

Next.js 16 introduces Cache Components and flips the default: with cacheComponents enabled, rendering is dynamic unless you explicitly say otherwise with the "use cache" directive.

// next.config.ts
const nextConfig: NextConfig = {
  cacheComponents: true,
};
import { cacheLife, cacheTag } from "next/cache";

async function getArticles() {
  "use cache";
  cacheLife("hours");
  cacheTag("articles");

  return loadArticlesFromDisk();
}

The directive can sit on a file, a component, or a function. The compiler generates the cache key from the inputs, so you no longer reason about which fetch options or route segment exports are silently influencing the result. Cache Components also replace the old experimental Partial Prerendering flag: static shell and dynamic holes are now the natural result of mixing cached and uncached components under <Suspense>.

New invalidation APIs

APIWhereWhat it does
revalidateTag(tag, profile)Server Actions, Route HandlersStale-while-revalidate invalidation. The second argument (a cacheLife profile such as "max") is now required.
updateTag(tag)Server Actions onlyExpires and refreshes immediately, so a user sees their own write on the next render ("read-your-writes").
refresh()Server Actions onlyRefreshes uncached data only, without touching the cache.

The practical rule: use updateTag when a user just changed something and expects to see it, and revalidateTag when eventual freshness is fine.

Our take: this is the most valuable change in the release, but it is opt-in for a reason. A content site like this one, with export const revalidate = 1800 on article routes so scheduled posts appear after their publish date, works fine on the classic model. Adopt Cache Components when you are starting a new app or when stale-data bugs are already costing you time. Don't migrate a working cache strategy just because a newer one exists.


Turbopack Is the Default

Turbopack is now the default bundler for both next dev and next build. The --turbopack flag in scripts like ours ("dev": "next dev --turbopack") is now redundant, and opting out requires --webpack.

What this means in practice:

  • Custom webpack config is the main migration risk. If next.config has a webpack() function, the build will flag it. Most common needs (SVG loaders, aliases) have Turbopack equivalents under the turbopack config key, but plugins that hook into webpack internals do not.
  • Filesystem caching for dev is available, which persists compilation results between restarts. On larger projects the restart-time savings are noticeable.
  • Production builds are where teams see the biggest difference, since that is the step that runs on every CI build and Docker image build.

middleware.ts Becomes proxy.ts

Middleware is renamed to proxy, and the rename is more than cosmetic. The name "middleware" suggested an Express-style chain where you could put auth, logging, and business logic. In practice it was always a network-boundary layer, and the new name says so.

// proxy.ts (was middleware.ts)
import { NextResponse, type NextRequest } from "next/server";

export function proxy(request: NextRequest) {
  if (!request.cookies.has("session")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
}

proxy.ts runs on the Node.js runtime. middleware.ts still works for Edge runtime cases but is deprecated. Keep proxy logic thin: redirects, rewrites, header manipulation. Authorization decisions about data belong next to the data, in the Server Component or Route Handler that reads it.


Breaking Changes That Will Bite You

Most upgrade pain comes from a short list:

1. Synchronous request APIs are gone. Next.js 15 introduced async params, searchParams, cookies(), headers(), and draftMode() with a compatibility shim. Version 16 removes the shim. Every dynamic route needs the async form:

// src/app/articles/[slug]/page.tsx
interface ArticlePageProps {
  params: Promise<{ slug: string }>;
}

export default async function Article({ params }: ArticlePageProps) {
  const { slug } = await params;
  // ...
}

If you deferred this migration in 15, it's mandatory now. The codemod handles most cases: npx @next/codemod@canary upgrade latest.

2. next lint is removed. next build no longer runs the linter, and the next lint command no longer exists. Call ESLint directly (this site uses eslint src --max-warnings 0) and make sure CI runs it as a separate step. Otherwise linting silently stops happening.

3. Image defaults changed. Three of them affect real apps:

  • images.qualities defaults to [75]. Any quality prop not in the list is coerced to the nearest allowed value. If your components use quality={85}, add it to the config explicitly, as this site does with qualities: [75, 80, 85].
  • minimumCacheTTL default rose from 60 seconds to 4 hours, which reduces revalidation load but means updated remote images take longer to refresh.
  • Local images with query strings now require images.localPatterns, and images.domains is deprecated in favor of remotePatterns.

4. Platform minimums. Node.js 20.9+ and TypeScript 5.1+. Check your Docker base image before checking anything else.

5. Removed APIs. AMP support, serverRuntimeConfig and publicRuntimeConfig (use environment variables), and the experimental PPR flag (replaced by Cache Components). Parallel route slots now require an explicit default.js.


Smaller Changes Worth Knowing

  • React Compiler support is stable via reactCompiler: true. It is not enabled by default because it increases build time, but it removes most manual useMemo and useCallback work.
  • Smarter routing. Shared layouts are downloaded once when prefetching many links, and prefetching only fetches the parts of a route not already in cache.
  • Next.js DevTools MCP exposes routing, cache, and error information to AI coding agents, which matters if your team uses the agentic workflows described in our article on agentic coding.
  • Build Adapters API (alpha) lets hosting platforms customize the build output, a step toward first-class support outside Vercel.

React 19.2 Underneath

Next.js 16's App Router ships with React 19.2, and a few React features change how everyday code is written:

  • Actions and useActionState. Forms can take an async function as their action, and React tracks pending state, errors, and results for you. A typical contact form that hand-manages isSubmitting, submitStatus, and submitError with separate useState calls (this site's form has nine useState hooks) collapses into one useActionState call. A fetch to a Route Handler is still the right choice when the request needs things a Server Action handles awkwardly, such as a client-side reCAPTCHA token and per-IP rate limiting.
  • useOptimistic shows the expected result immediately and rolls back automatically if the action fails.
  • ref as a regular prop. forwardRef is no longer needed for new components.
  • <Activity> (new in 19.2) hides part of the UI while preserving its state, which suits tabs and back-navigation that should not lose form input.
  • useEffectEvent (new in 19.2) separates "react to this change" from "read the latest value", which fixes the class of effects that re-run because a callback was in the dependency array.

None of these force a rewrite. Adopt them where you're already touching a component.


A Practical Upgrade Checklist

  1. Upgrade Node.js in your Docker image and CI to 20.9+.
  2. Run the codemod: npx @next/codemod@canary upgrade latest.
  3. Search for remaining synchronous params, searchParams, cookies(), and headers() access.
  4. Replace next lint with a direct ESLint call, and add it to CI if it only ran inside next build.
  5. Audit quality props against images.qualities, and check for images.domains.
  6. Remove --turbopack flags. If you have a webpack() config, port it or temporarily build with --webpack.
  7. Rename middleware.ts to proxy.ts unless you need the Edge runtime.
  8. Build the production image and smoke-test it, not just next dev. Standalone output (output: "standalone") behaves differently from the dev server, and that's where configuration mistakes surface.
  9. Leave Cache Components for a separate, deliberate change.

Final Takeaway

Next.js 16 removes ambiguity. Caching is something you declare, not something you discover. The network boundary is named for what it is. The deprecated compatibility shims are gone. For most App Router projects already on 15, the upgrade is a day of mechanical changes followed by a decision about whether to adopt Cache Components. Make that decision separately and on its own merits, because explicit caching is where the long-term value is, but it's also the change that deserves the most careful testing.

Related reading: Next.js 15: Revolutionary Features That Will Transform Web Development · Building Scalable Microservices with Docker and Kubernetes