For years the most common Next.js complaint was "why is this cached?". Next.js 16 answers with Cache Components: nothing is cached unless you say so, and when you do, you say for how long and how to invalidate it.
Recent 16.x releases have built on it with partial prefetching, faster client navigations and, for new apps, Cache Components switched on by default. If you maintain a Next.js 15 app, here's how to migrate without breaking production.
The model in five lines
- Rendering is dynamic by default: data is fetched per request
- Add
'use cache'to a component, function or file to cache its output - Use
cacheLife()to set how long a cached entry stays fresh - Use
cacheTag()to label entries and invalidate them withupdateTag()orrevalidateTag() - Partial Prerendering serves a static shell instantly and streams dynamic holes inside
<Suspense>
The use cache reference is the canonical documentation.
Enabling it
// next.config.ts
import type { NextConfig } from "next";
const config: NextConfig = {
cacheComponents: true,
};
export default config;
With the flag on, Next.js will tell you during development and build where uncached data is accessed outside a <Suspense> boundary. That error is a feature: it shows you exactly which parts of the page block the shell.
A typical before and after
Before (Next.js 15, implicit):
export const revalidate = 3600;
export default async function Page() {
const products = await getProducts();
return <ProductGrid products={products} />;
}
After (Cache Components, explicit):
import { cacheLife, cacheTag } from "next/cache";
async function getProducts() {
"use cache";
cacheLife("hours");
cacheTag("products");
return db.product.findMany();
}
export default async function Page() {
return (
<>
<Hero />
<Suspense fallback={<GridSkeleton />}>
<ProductGrid products={await getProducts()} />
</Suspense>
</>
);
}
And in the server action or webhook that changes products:
import { updateTag } from "next/cache";
export async function saveProduct(data: FormData) {
"use server";
await db.product.update(/* ... */);
updateTag("products");
}
Migration plan, route by route
- Upgrade to Next.js 16 first, without enabling the flag. Fix async request APIs (
cookies(),headers()andparamsare async) and renamemiddleware.tstoproxy.tswhen you're ready - Inventory your caching: every
revalidateexport,fetchcache option,unstable_cacheanddynamic = "force-static" - Enable
cacheComponentson a branch and let the dev overlay list the blocking data accesses - Push dynamic data down: wrap per-user or per-request parts in
<Suspense>so the rest of the page can prerender - Replace
unstable_cachewith'use cache'functions pluscacheTag - Map invalidation: for every write path, decide which tags it updates
- Ship one route at a time behind preview deployments, and compare TTFB and LCP before and after
Pitfalls we've hit
- Caching user-specific data by accident. Anything that reads cookies or headers must not sit inside a shared
'use cache'scope. Pass the values in as arguments, or use the private cache variant for per-user data - Over-tagging. Hundreds of tags per page make invalidation slow and hard to reason about. Tag by entity type and ID (
product:123), not by page - Serverless cold paths. Cached entries are per deployment and region. Warm critical pages after deploy if your traffic is spiky
- Third-party SDKs that read headers deep inside a component you thought was static. The build error tells you; listen to it
Is it worth it?
On content-heavy and e-commerce pages we've seen meaningfully faster TTFB and LCP once the shell is static and only the cart, pricing or personalisation streams in. That feeds straight into Core Web Vitals. See our INP guide for the interaction side.
More importantly, the caching behaviour is now readable in the code, which makes onboarding new engineers, and AI coding tools, far easier.
Bottom line
Start new apps with Cache Components. Migrate existing apps route by route, starting with your highest-traffic marketing or catalog pages, and use the build errors as your checklist. Our Next.js production checklist covers the rest of the launch hygiene.
Need a hand? Our web development team runs Next.js upgrades as fixed-scope sprints. Talk to us.
Frequently asked questions
What are Cache Components in Next.js?
Cache Components is Next.js's explicit caching model. Nothing is cached by default; you opt in with the 'use cache' directive on components, functions or pages, set lifetimes with cacheLife and invalidate with cacheTag, combined with Partial Prerendering for static shells.
Do I have to migrate to Cache Components now?
Not immediately. It is opt-in for existing apps in Next.js 16 and is expected to become the default in a future major version. New projects should start with it; existing apps can migrate route by route.
What replaced middleware.ts in Next.js 16?
Next.js 16 introduced proxy.ts as the successor to middleware.ts, clarifying that it runs at the network boundary in front of your app. Existing middleware files keep working during the deprecation period.