Next.js 16.4 shipped on 6 October 2026, and its headline is not a single feature but a change in advice. After several 16.x releases in which Cache Components were optional and came with caveats, the Next.js team now recommends Cache Components for every Next.js app. New projects created with create-next-app get it switched on by default, and the team has said it will become the default for everyone in Next.js 17.
That makes this release a good moment to understand the model properly, even if you are not upgrading this week. This post covers what Cache Components are, the two new tools in 16.4 for controlling what renders when (ensureStatic and the navigation()/prefetch() functions), the improvements every app gets just by upgrading, the experimental flags worth knowing about, and a sensible upgrade plan for an existing App Router project.
Cache Components in one paragraph
The original App Router cached a lot of things implicitly, and many developers found it hard to predict whether a page was static, dynamic or somewhere in between. Cache Components replace that with explicit, opt-in caching. You mark a component or function with the 'use cache' directive and optionally give it a lifetime with cacheLife(). The Next.js announcement describes 'use cache' as “a component-level version of the Cache-Control HTTP header”. Anything you do not mark is rendered at request time. A single page can mix both: a prerendered, cached article and a per-user avatar can stream together in one response.
In 16.4 terms, “Cache Components” means two config flags working together:
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
};
export default nextConfig;
Partial Prefetching arrived in 16.3, and the team now treats it as part of the same model. With it enabled, a navigation happens in stages. A default <Link> loads the route’s App Shell (the part that does not depend on the URL). A <Link prefetch> can also load content that depends on that specific link. The rest of the route renders when the user actually clicks. The new APIs in 16.4 are all about choosing which stage each piece of your page belongs to.
Why the recommendation changed now
The release post is candid that Cache Components were not previously recommended for every app, because there were cases where they could not match the cost and performance guarantees of the older model. Two of those gaps were about control. If you wanted a page that is guaranteed static, there was no simple way to stop someone from adding a dynamic component later. And if you used aggressive prefetching, every visible link could trigger expensive work for content the user might never open. 16.4 adds an answer to each.
ensureStatic: make “this page stays static” a build rule
Static pages are cheap to serve and easy to cache at a CDN. The risk with a mixed model is that one innocent change, such as reading cookies() in a shared header, quietly makes a whole route render per request. ensureStatic is a route segment config you export from a page or layout to turn that risk into a build failure.
// app/blog/layout.tsx
export const ensureStatic = 'navigation';
export default function BlogLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
According to the API reference, it accepts five values:
| Value | What must be static |
|---|---|
'auto' (default) |
Nothing extra; same as not exporting it |
'shell' |
The App Shell loaded by a default <Link> |
'prefetch' |
The App Shell and per-link prefetches |
'navigation' |
The complete server-rendered route |
false |
Opts the segment out, and fails the build if another segment tries to apply a static level |
A few details from the docs are worth knowing before you add it:
- It only works with
cacheComponentsenabled. Without it, the build fails. 'shell'and'prefetch'depend on Partial Prefetching. Without it they add no requirement (and'shell'logs a warning).'navigation'works with Cache Components alone.- With
'navigation', uncachedfetchor database calls,connection(),cookies(),headers()and server-sidesearchParamsall fail validation, even inside<Suspense>. So does a'use cache'scope with a very short lifetime (for example anexpireunder five minutes). The same rules apply togenerateMetadata. - Dynamic routes using
'navigation'must exportgenerateStaticParams()and return at least one complete set of parameters. - A child segment can be stricter than its parent but not looser. A page under a
'prefetch'layout can set'navigation'; setting'shell'fails the build. - Client Components can still load browser-only content inside
<Suspense>, for example with React’s newuse(browser()).
Validation for 'navigation' runs in both next dev and next build, and the docs suggest next build --debug-prerender when build errors are too terse. For blogs, documentation sites, marketing pages and store catalogues, setting ensureStatic = 'navigation' on the root layout and relaxing it on specific subtrees later is a reasonable default. Highly personalised dashboards will rarely need it.
navigation() and prefetch(): defer work to a later stage
The second gap was prefetch cost. If an inbox renders fifty <Link prefetch> elements, each one can load its target page’s cached data as soon as it scrolls into view. That removes loading states, but it may also mean a lot of database work for messages nobody opens.
16.4 adds two functions from next/cache that you await inside a Server Component:
await navigation()keeps everything below it out of the App Shell and out of per-link prefetches. It renders when the user navigates.await prefetch()keeps everything below it out of the App Shell, but allows it into an explicit per-link prefetch.
Here is a trimmed version of the pattern from the docs. The comments on a post are not loaded by any prefetch; they wait until the user actually opens the post, while still being cached per post and sort order:
// app/post/[slug]/page.tsx
import { Suspense } from 'react';
import { cookies } from 'next/headers';
import { cacheLife, navigation } from 'next/cache';
export default function Page({ params }: PageProps<'/post/[slug]'>) {
return (
<Suspense fallback={<p>Loading comments...</p>}>
<Comments params={params} />
</Suspense>
);
}
async function Comments({ params }: { params: Promise<{ slug: string }> }) {
await navigation(); // everything below waits for a real navigation
const { slug } = await params;
const sort = (await cookies()).get('comment-sort')?.value ?? 'newest';
const items = await getComments(slug, sort);
return <CommentList items={items} />;
}
async function getComments(slug: string, sort: string) {
'use cache';
cacheLife('hours');
return db.comments.list({ slug, sort });
}
Three rules from the API reference trip people up:
- Wrap the component that awaits
navigation()in<Suspense>, so the prefetch has a fallback to show. Calling it outside a boundary is flagged in development. - Do not call it inside a cached scope. Put
'use cache'on a function called below it, asgetCommentsdoes. Calling it inside'use cache',unstable_cache(),after(),generateStaticParamsor a Client Component throws. - It is not the same as
connection().connection()says “this needs a real request” and makes the work request-dependent.navigation()only says “not during prefetch”, so the work below can still be cached and reused.
Both functions require Cache Components and throw without it. If you tried them in earlier canaries, note that navigation replaces the unstable_navigation name.
Improvements every app gets by upgrading
Several changes need no code at all, according to the release notes:
- Smaller dev cache. Turbopack’s disk cache uses 20-25% less space, now using Zstandard compression for bulk data while keeping LZ4 for metadata.
- Lazy server HMR. Editing a shared server module no longer recompiles every page you visited earlier in the session; other routes update when they are next requested.
- Shared Turbopack runtime. The runtime ships as one chunk shared across routes, which improves cache hit rates.
- Smaller production bundles. Shorter CSS Module class names in production builds (dev keeps the readable ones) and export-name mangling between modules.
- React 19.3. This brings stable View Transitions and Fragment Refs, plus a new
browser()API fromreact-domthat, used asuse(browser()), renders the nearest Suspense fallback on the server and the real content only in the browser. That is a cleaner option for things like reading the user’s time zone than the old “mounted” state trick.
Experimental flags worth knowing
These are all under experimental in next.config.ts and should be tried on a branch first:
turbopackRustReactCompiler: true(withreactCompiler: true) runs the Rust port of the React Compiler inside Next.js instead of through Babel. The team reports 30% lower compiler memory use and 15% faster compile time from allocation changes in this release.turbopackGc: truegarbage-collects unused compilation work from memory and the disk cache during long dev sessions.turbopackLazyDynamicImports: truecompiles client-sideimport()targets only when the browser asks for them. Somenext/dynamicimports are still compiled eagerly.turbopackPluginRuntimeStrategy: 'workerThreads'runs Babel, PostCSS and webpack loaders in worker threads instead of separate processes. Note the caveat in the release post: on Node.js 24.13.1 and newer it currently falls back to child processes because of a Node.js bug.turbopackAdditionalRootslets Turbopack follow symlinked packages outside the project root, which helps with monorepo-style local packages and pnpm’s global virtual store.
The Turbopack Bundle Analyzer also gained a route summary page, a sortable table view, saved snapshots you can diff over time, and a filter for modules on the critical render path. If bundle size is a recurring argument on your team, the snapshot diff alone is a good reason to try it.
The agent features, briefly
16.4 adds tooling aimed at AI coding agents. npx next@canary upgrade --agent=latest prepares migration guides, codemods and verification steps for an agent to apply. experimental.agentUpgrade makes next dev and next build remind you about upgrades; its default policy, 'security', only flags releases that fix vulnerabilities affecting your installed version, and false turns it off. experimental.agentFeedback lets an agent draft issue reports that you review before anything is sent; it is on by default for new create-next-app projects, needs telemetry enabled, and does not run in CI.
My recommendation is to treat the security reminder as useful and the rest as optional. Next.js has published several security releases in August and September 2026, so an upgrade nudge that only fires for vulnerabilities is worth having. Whether you let an agent perform the upgrade is a team decision; either way, review the diff as you would any other pull request.
How to approach the upgrade
For a new project, there is little to decide: create-next-app now starts you on Cache Components, which is where the framework is heading.
For an existing App Router app, a staged approach keeps risk low:
- Upgrade first, change nothing else. Run
npm install next@latest(16.4 ships with React 19.3), run your tests and build, and check that nothing regressed. You get the Turbopack and bundle-size gains without touching your caching model. - Turn on
cacheComponentson a branch. Expect build errors pointing at data access that is neither cached nor inside<Suspense>. Each one is a decision: cache it with'use cache', or wrap it in a boundary so it streams at request time. - Add
partialPrefetchingonce the app builds cleanly, then look at your heaviest<Link prefetch>lists and decide what belongs behindnavigation(). - Lock in what should never be dynamic with
ensureStatic, starting with content-heavy sections such as a blog or docs.
You do not have to finish all four before Next.js 17. But the direction is now clear, and moving one route group at a time is far easier than doing it under the pressure of a major-version upgrade.
Sources: Next.js 16.4 release post, Next.js docs: ensureStatic, Next.js docs: navigation(), Next.js docs: Caching, React 19.3 announcement, Next.js blog (security releases)