Next.js 16 makes caching explicit. Instead of a route being "static" or "dynamic" as a whole, Cache Components let one page mix prerendered UI, cached data and request-time content. We moved this very website onto it, and the model is simpler than it first looks.
The mental model
Everything is dynamic unless you say otherwise. You opt pieces into caching with the "use cache" directive, and anything that genuinely depends on the request (cookies, headers, search params) must sit inside a <Suspense> boundary so the rest of the page can still be prerendered as a static shell.
// next.config.ts
const nextConfig = { cacheComponents: true };
export default nextConfig;The four APIs you'll actually use
"use cache"— marks a function or component as cacheable. Its return value is stored and reused.cacheLife(profile)— how long a cached result lives. Presets includeminutes,hours,days,weeksandmax.cacheTag(tag)— labels cached data so you can invalidate it later.revalidateTag(tag, profile)— marks tagged data stale. Use"max"for stale-while-revalidate, or{ expire: 0 }when the next visitor must see fresh content.
export async function getSiteContent() {
"use cache";
cacheLife("hours");
cacheTag("sanity");
return client.fetch(SITE_QUERY);
}How we wire CMS publishing to it
Our content lives in a headless CMS. Every page reads it through cached functions tagged sanity. When an editor publishes, the CMS calls a signed webhook, and the route handler runs revalidateTag("sanity", { expire: 0 }). Pages stay fully static for speed and SEO, yet edits appear on the very next request. The hours lifetime is only a safety net in case a webhook is ever missed.
Gotchas we hit
- Current time in prerendered components. Calling
new Date()in a component that gets prerendered fails the build. Read it in an effect, or use an external store with a fixed server snapshot. - Params outside Suspense. For dynamic routes, reading
paramsat the top of the page blocks the instant shell. Either read them inside<Suspense>, or, when every slug is known at build time, opt out withexport const instant = falseto keep proper 404 status codes. - generateStaticParams must return something. An empty array is a build error under Cache Components. For a blog that might be empty, return a placeholder param that resolves to a 404.
- dynamicParams is gone. Unknown params render on demand; call
notFound()when the data doesn't exist.
Think of Cache Components as "static by declaration": you decide what is cached, for how long, and what invalidates it.
Is it worth migrating?
If your site mixes marketing pages with personalised or frequently changing content, yes. You get static-site speed where it matters and precise, tag-based freshness everywhere else. Start by enabling the flag, fix the build errors it surfaces (they are usually real bugs), then add "use cache" to your data layer one function at a time.

