This is the fifth post in the series after Next.js 16, 16.1, 16.2, and 16.3. Next.js 16.4 (opens in new tab) came out today, October 6, and the one-line summary is that Vercel has stopped hedging: Cache Components are now the recommended model for every Next.js app, on by default in create-next-app, and the default for everyone in Next.js 17.
We upgraded this site the same afternoon. The bump took a minute, the monorepo did its usual duplicate-Next trick, builds came out flat to slightly faster, and the production JavaScript came out 5% smaller without us touching anything. The new agent upgrade command fell over on our config the first time, and on the second try handed us a task whose first step would have found our own parked PR.
What's new and shiny
Most of the release is Cache Components growing the last few pieces it needed before Vercel would recommend it universally. The rest is agent tooling, Turbopack trimming, and React 19.3.
Cache Components are now the recommended model
If you've skipped the last three posts: Cache Components is the name for the programming model behind the two flags we've been writing about since 16.3.
The model in one paragraph. Every component in your tree either renders at request time (the default) or is marked with 'use cache', which tells Next.js to cache that component's output and reuse it: in the browser between client navigations, and optionally on the server or at build time. Vercel's own description is that 'use cache' is a component-level version of the Cache-Control HTTP header, which is the clearest explanation we've seen. You set a lifetime with cacheLife('hours'), and the cached and uncached parts of a page stream together in one response.
The greeting renders per request; the project list is cached for hours and served instantly. That composition is the whole point, and it replaces the implicit caching rules of older App Router versions that nobody could keep in their head.
What changed in 16.4 is the recommendation. Through 16.3, Vercel said the model couldn't match the old one on cost and performance in some cases, so it wasn't recommended for everyone. The two features below close those cases, and Partial Prefetching (which shipped separately in 16.3) is now considered part of the model rather than an add-on.
ensureStatic: a build-time lock on static routes
The gap for sites like ours was this. Cache Components lets you mix static and dynamic freely, which is great for a dashboard and a liability for a blog. One developer adds a <UserAvatar /> to a layout, and every page under it quietly starts rendering at request time. Nothing fails. The bill goes up and the pages get slower, and you find out later.
ensureStatic is the lock. Export it from a page or a layout and the build fails if anything dynamic sneaks into the output it covers.
Three strictness levels, each including the ones before it:
| Value | What must be static |
|---|---|
'shell' | The App Shell a default <Link> loads |
'prefetch' | The shell plus per-link prefetches |
'navigation' | The complete server-rendered route |
'navigation' is the one a content site wants. With it set, the build rejects uncached fetch or database calls, cookies(), headers(), server-side searchParams, any 'use cache' scope whose lifetime is under five minutes, and any request-dependent promise handed to a Client Component. It rejects them even inside <Suspense>, and even on a route that set instant = false. Dynamic routes have to export generateStaticParams() with at least one complete parameter set, or the build fails. It also applies to generateMetadata, which is where dynamic reads like to hide.
The sensible setup is 'navigation' on the root layout, then relax it on a nested layout when a section needs request-time content. A child can set a stricter level than its parent but never a weaker one, so the guarantee can't be undone by accident further down the tree.
Two things worth knowing. It only works with cacheComponents: true, and the build fails if you export it without the flag. And it's a separate concept from instant: instant asks whether the UI can update immediately (cached UI passes even if request-time content streams in afterwards), while ensureStatic asks whether there's any request-time work at all. A page can be instant and still cost you a server render per visit; ensureStatic = 'navigation' is how you forbid the render.
navigation() and prefetch(): choosing what a prefetch loads
The other gap was prefetch cost. Partial Prefetching in 16.3 gave each route one shared shell, and <Link prefetch> gave you the opposite extreme, where a link prefetches everything cached on the target page before the user clicks. For a page with a lot of cached data (Vercel's example is an inbox where each message page loads a full thread), prefetching every visible link means loading every thread for messages nobody opens.
16.4 adds a middle setting, per subtree. await navigation() at the top of a component excludes everything below it from the prefetch, so it renders only when the user actually navigates:
Now a visible link prefetches the first message and nothing else. The thread loads on click, behind the spinner. await prefetch() is the same idea one stage earlier: it keeps a subtree out of the shared App Shell so it only renders during an explicit <Link prefetch> or a navigation. Together with ensureStatic you get the full dial, from "never load this early" to "this must be static or the build dies".
No spam, only good stuff
Subscribe, for more hot takes
next upgrade --agent
16.4's agent story moves from docs and skills to the upgrade itself. The new --agent flag on next upgrade reads your installed version, picks a target from a policy, copies the matching migration guides and skills into a temporary directory, and hands the whole thing to a coding agent as a task.
The @canary is deliberate: it runs the newest upgrade tooling regardless of what version your app is on, and it only picks a canary target if your app was already on canary. Three policies: security (the default, moves an affected stable version to the nearest safe release), latest (moves to npm's latest), and experimental-future, which does latest and then prepares the Cache Components adoption guide and skill. If you run the command from inside Codex or Claude Code, it prints the task for that agent; from a plain terminal it offers to start one, including a separate Git worktree if you want it.
The companion is experimental.agentUpgrade in next.config. With it set, next dev and next build nudge you (or your agent) when a relevant upgrade exists, with the policy deciding what counts as relevant: 'security' only flags versions with known vulnerabilities, 'latest' flags any newer minor or major, false turns it off. The reminder doesn't upgrade anything on its own; it buffers the running command's output and offers Upgrade now, Skip, or Skip until next version.
We've been running every client project on patched stable with a monthly bump since the security cadence started in July. A reminder at next dev time with a ready-made agent task behind it is close to what we'd have built ourselves. More on what it printed for us below.
Agent feedback
The other new agent feature is experimental.agentFeedback. With it on, next dev tells your coding agent to keep a note of framework errors, unclear docs, and workarounds it hits during a task. When the task is done, the agent drafts reports and opens them in your browser. You read them, edit or bin each one, and nothing goes anywhere until you press Send feedback. The agent is instructed to leave out source code, logs, secrets, and project detail.
It's on by default for new create-next-app projects, opt-in for existing ones, needs Next.js telemetry enabled, and doesn't run in CI. Vercel's pitch is that they learn from how agents actually build apps. Ours is that an agent which hits a framework bug mid-task usually works around it and never mentions it, and this is the first mechanism we've seen that captures those workarounds instead of losing them.
Turbopack: smaller cache, lazier HMR, smaller bundles
Four improvements that need no configuration.
The disk cache is 20-25% smaller. Turbopack switched the bulky data to Zstandard compression and kept LZ4 for metadata so lookups stay fast, plus better compaction of stale entries. We measured ours below.
Lazy server HMR. Editing a shared server module used to recompile every page you'd visited that session, whether you were looking at it or not. Now the page you're on updates and the others wait until you request them again.
One shared runtime chunk across routes instead of per-route copies, which helps download size and cache hit rates.
Smaller production bundles. CSS Module class names are shorter in production (development keeps the readable ones), and exports between modules get mangled to short names.
React 19.3
Next.js 16.4 ships React 19.3 (opens in new tab), out since September 9. The headline is View Transitions going stable: wrap part of the UI in <ViewTransition> and React animates enters, exits, moves, and resizes through the browser's View Transition API whenever a Transition updates them, with addTransitionType to pick a different animation for forward versus back. Also stable: Fragment Refs (a ref on a <Fragment> that gives you event, focus, and measurement methods over a group of DOM siblings without a wrapper element), the browser() API (use(browser()) in a component shows the Suspense fallback on the server and renders for real after hydration, for anything that needs localStorage or the user's timezone), Trusted Types support, and rendering a Context directly from a Server Component without a Provider wrapper.
Next's peer range still accepts any React 19, so existing apps bump react and react-dom themselves. We did, and it was uneventful.
The experimental grab bag, explained simply
Six flags, all under experimental in next.config.ts, none of which you need to touch.
Rust React Compiler, round two. The Rust port of the React Compiler from 16.3 now has a fast check that skips files with nothing to optimize, and it no longer repeats the optimization when a Client Component is built again for server rendering. The Turbopack team's memory allocation changes cut the compiler's memory use by 30% and compile time by 15%. Flag is still turbopackRustReactCompiler: true alongside reactCompiler: true.
turbopackGc. Long dev sessions accumulate compiled work for code you've since changed or routes you've deleted. This is a garbage collector for that, in memory and on disk, and it can reclaim stale data from earlier sessions too.
turbopackLazyDynamicImports. Code behind an import() normally still gets compiled up front in development. With this on, client-side dynamic imports compile when the browser first asks for them, so a library you load after a button click costs nothing until the click. Some next/dynamic imports still compile eagerly.
turbopackPluginRuntimeStrategy: 'workerThreads'. Babel, PostCSS, and webpack loaders currently run in separate Node processes talking to Turbopack over sockets. Worker threads put them in one process. One caveat straight from the release notes: on Node.js 24.13.1 and newer it falls back to child processes because of a Node bug, so check your Node version before expecting anything from it.
turbopackAdditionalRoots. Lets Turbopack follow symlinked dependencies outside your project root, which is for people working on linked local packages, and for pnpm's global virtual store (add the directory pnpm store path prints). Automatic integration is planned; today it's manual.
Bundle Analyzer, plus a skill. The Turbopack Bundle Analyzer now opens on a summary of your largest client routes, has a table view sorted by what contributes most, snapshots every analysis so it can diff bundles over time, and can separate the critical render path from async dependencies. There's also a next-bundle-optimizer agent skill (npx skills add vercel/next.js --skill next-bundle-optimizer) that finds and fixes the oversized bits for you. Given that a 400KB particle effect was the biggest performance win we found all year, this is the one we'll actually run.
Feature status at a glance
| What | Status in 16.4 |
|---|---|
Cache Components (cacheComponents + partialPrefetching) | Recommended for all apps; default in create-next-app; default in Next.js 17 |
ensureStatic route segment config | Stable, requires cacheComponents |
navigation() and prefetch() from next/cache | Stable, requires cacheComponents |
next upgrade --agent | Experimental, via next@canary |
experimental.agentUpgrade reminders | Experimental, default policy 'security' |
experimental.agentFeedback | Experimental, default on for new apps |
| Smaller disk cache, lazy server HMR, shared runtime, shorter class names | On by default |
| React 19.3 | Installed by create-next-app; existing apps bump manually |
| Rust React Compiler | Experimental, turbopackRustReactCompiler |
turbopackGc, lazy dynamic imports, worker threads, additional roots | Experimental |
Turbopack Bundle Analyzer + next-bundle-optimizer skill | Available |
Our upgrade experience
We bumped this site (a pnpm monorepo with four Next.js apps and a shared UI package, all pinned to exact versions) from 16.3.0 and React 19.2.3 to 16.4.0 and React 19.3.0 about two hours after the release post went up. Five package.json edits and a pnpm install, 41 seconds.
Then the typecheck failed, and we knew the error before we read it:
Two copies of Next.js in one type graph, for the third release running. The source this time is @workflow/web, a transitive dependency of the workflow package, which pins next to exactly 16.0.10 as a regular dependency. We'd already overridden that to 16.3.0 in pnpm-workspace.yaml back in August, and that override had quietly become the thing holding a 16.3.0 copy in the tree.
One character changed, reinstall, one next in the lockfile, typecheck green. If you're in a monorepo and your typecheck breaks after the bump, it's a duplicate next. It was a duplicate next in June, it was a duplicate next in August, and it's a duplicate next now. Run pnpm why next before you read the error message.
No code changes. The edge runtime deprecation warning on our OG image route is still there from 16.3 and still just a warning. We left @types/react on 19.2.8; the 19.3.0 types are published if you want ViewTransition typed, but we aren't using any of the new APIs yet.
Build numbers
Same machine (Apple Silicon), same content, 1,555 static pages, .next wiped before every cold build. We ran each configuration more than once, because the first build after a pnpm install is slower than the ones after it whatever version you're on.
| Build | 16.3.0 | 16.4.0 |
|---|---|---|
First build after install, .next wiped | 83.9s total / 15.9s compile | 49.1s / 12.0s |
Later builds, .next wiped | 30.2s / 7.3s | 25.3-30.2s / 6.5-6.9s |
| Repeat build, cache warm | 20.9s / 1.2s | 20.6s / 1.7s |
.next/cache on disk | 282MB | 239MB |
Compile time is within a second either way once the machine is warm, and the warm repeat build hasn't moved. The first-build-after-install gap is large on our runs but we only have one of them on 16.4, so treat it as a hint rather than a result. The number we trust is the cache directory: 282MB down to 239MB, a 15% cut from the Zstandard switch. Vercel quoted 20-25% for the dev cache; this is the build cache, same compression, on a site that's mostly static pages.
Production bundles
Same two builds, measured on disk in .next/static, uncompressed, across every chunk:
16.3.0 | 16.4.0 | Change | |
|---|---|---|---|
| JavaScript, all chunks | 6,343KB | 6,033KB | -310KB (-4.9%) |
| CSS, all stylesheets | 280KB | 277KB | -1% |
| Chunk count | 81 JS / 4 CSS | 81 JS / 4 CSS | Unchanged |
Export mangling got us 310KB of JavaScript off disk without a code change. CSS is flat because the site is Tailwind with no CSS Modules, so the shorter class names had nothing to shorten. That's the whole 16.4 payload story for us: a free 5%, after the 341KB we cut by hand in July.
What next upgrade --agent did for us
We ran it the way the docs show for a workspace root, pointing at the app directory:
It stopped immediately:
Our next.config.ts imports a relative TypeScript module. The canary CLI transpiles the config before it does anything else, and when it's pointed at a subdirectory it resolves that relative import from the wrong place. Running the same command from inside apps/web worked, and reported the correct, slightly deflating answer: "Next.js 16.4.0 is already the latest stable release." We'd done its job by hand two hours earlier.
So we tried the policy that applies even when you're current:
That printed the full task into our terminal: read shared.md first, then future-defaults.md, then the Cache Components migration guide and the adoption skill prompt, all copied into a temp directory, with a telemetry command to run at the end reporting success or failure. The first checklist item in shared.md is to look for duplicate work before touching any file: local branches, remote branches, and gh pr list for open PRs doing the same thing, with a diff inspection of likely matches.
Had we let an agent run it, step one would have found PR #767 (opens in new tab), open since July 9, titled "feat: adopt cache components and instant navigations", and stopped there. Which is the right answer, and a mildly embarrassing one to receive from a CLI. We stopped at the prompt rather than run the task, since the parked branch is a decision for the next section, not a tool.
The crash-test-dummy note, as ever: this bump goes to production with this post. Client projects get it at the next monthly security bump, after it's had a few weeks in the wild on ours.
The parked PR, a quarter later
In the 16.3 post we built the full Cache Components adoption for this site on a branch, clicked around it for a week, and merged none of it, because a fully static site already navigates instantly. The PR has been sitting open since July.
16.4 removes two of the reasons we gave. The first was the lack of a guard: once Cache Components is on, nothing stops a future change from making a page dynamic, and on a site that gets most of its traffic from static files that's a cost we didn't want to police by hand. ensureStatic = 'navigation' on the root layout is exactly that guard, and it fails the build rather than the bill. The second was the soft-404 shell behavior we had to noindex around: with 'navigation' set, a dynamic route that gets a parameter it didn't prerender now waits for the static result instead of serving a shell with fallbacks in it.
What hasn't changed is the thing that made us park it. Static files are still static files, and no 16.4 feature makes a prerendered page navigate faster than it already does. So the verdict today is the same as July, with a smaller asterisk: if we ever do adopt, 16.4 is the version to do it on, with ensureStatic set from day one. We haven't rebased the branch onto 16.4 yet. When we do, that's the next post.
Should you enable it?
Depends what you run, same as last time, but the answer has moved for two groups.
New apps: it's on already. create-next-app enables both flags, and the discipline it enforces (every route declares stream, cache, or block) is much easier to live with from the first commit than to retrofit. Don't turn it off.
App-like sites (dashboards, feeds, anything personalized): this was already the model for you in 16.3. In 16.4 you also get navigation() to stop eager prefetches loading data nobody looks at, which was the main complaint we heard about <Link prefetch>. Adopt it.
Content sites that can go fully static: you still have instant navigations already, and they're still called static files. The difference now is that adopting Cache Components no longer puts your static guarantee at risk, because ensureStatic = 'navigation' on the root layout turns "someone added a dynamic component" into a failed build. If you're going to adopt anyway (and Next.js 17 will make that decision for you), 16.4 is the version to do it on. If you're not, spend the time on the bundle analyzer. We'd still bet money you have a 400KB something.
Everything in between: read the ensureStatic levels table twice. 'shell' and 'prefetch' are cheap to add and don't validate at build time; 'navigation' is the one with teeth. Start at 'prefetch' on the layouts that matter and tighten from there.
Quick comparison
| Next.js 16.3 | Next.js 16.4 | |
|---|---|---|
| Cache Components | Opt-in, not universally recommended | Opt-in for existing apps, default for new ones, recommended for all |
| Static guarantee | None once the flags are on | ensureStatic at 'shell', 'prefetch', or 'navigation' |
| Prefetch granularity | Per route (shell) or per link (everything cached) | Per subtree, via navigation() and prefetch() |
| Upgrading | next upgrade codemods, or by hand | next upgrade --agent with a policy, plus reminders in next dev |
| Agent feedback to Vercel | GitHub issues, by a human | Agent-drafted reports you review and send |
| Turbopack disk cache | LZ4 throughout | Zstandard for data, LZ4 for metadata, 20-25% smaller |
| Server HMR | Recompiles every visited page | Only the page you're on |
| CSS Module class names in production | Long | Short |
| React | 19.2 | 19.3: View Transitions and Fragment Refs stable, browser() |
| Rust React Compiler | Experimental | Experimental, 30% less memory, 15% faster |
How to upgrade
Three routes again.
Option 1: bump it yourself. This is what we did, and the whole thing is below.
Option 2: let the agent do it. Run this from inside your coding agent and it prints the upgrade task; run it from a terminal and it offers to start Codex or Claude Code for you:
Swap latest for experimental-future if you also want it to prepare the Cache Components migration. The command is experimental, so read the task before letting the agent loose on it.
Option 3: start fresh. npx create-next-app@latest gives you 16.4 with Cache Components, Partial Prefetching, React 19.3, and agent feedback all on. For a greenfield project that's the configuration Vercel now recommends, and it's what we'd start a new client build on.
If you flip the flags on an existing app, expect errors on day one, same as 16.3: each one is a route that's quietly slow today. The new part is that if you also add ensureStatic = 'navigation', the errors come from the build instead of from the DevTools panel, and they don't go away until the route is actually static.
Closing thoughts
Next.js 17 makes Cache Components the default, Vercel hasn't put a date on it, and the experimental-future policy is their early version of how that migration gets handed to an agent. Our next step is literal: rebase PR #767 onto 16.4, add ensureStatic = 'navigation' to the root layout, let the adoption skill redo the mechanical parts, and see whether the build stays green. That's the next post, parked PR and all.







