Cache Components in Next.js
This article is part of a series on Next.js Cache Components. See all articles: Cache Components in Next.js · Data-Level vs. UI-Level Caching · How Revalidation Works with "use cache" · Migrating to Cache Components
One of the projects Stefan and I have worked on for years is bademeister.com, the official website of the German band Die Ärzte. The site has a guestbook. Yes, a guestbook, still alive in 2026 and heavily used by fans. The first entries go back to 2006, and there are over 1,000 pages of content.
During the Pages Router era, we used Incremental Static Regeneration (ISR)) for most of the site. News, tour dates, discography, all cached with getStaticProps and revalidate. When content changed, we called response.revalidate() on the NextApiResponse object to invalidate specific paths. That worked for content with known URLs.
But the guestbook was different. Over 1,000 paginated pages where any new entry shifts the pagination. To revalidate after a new post, we would have had to call revalidate() for every single page path. So we never cached the guestbook and loaded it dynamically on every request.
The invalidation problem
In the Pages Router, the framework knew the URL but had no idea what data went into rendering a page. If /guestbook/page/42 called getEntries() and getPageCount(), the framework didn't know about those function calls. It just knew that the path produced some HTML.
Invalidation was path-based. You had to figure out which routes depended on which data, then call revalidate() for each one. For a paginated guestbook where any new entry affects all subsequent pages, that's not workable.
App Router: tag-based invalidation
The App Router in Next.js 13 changed this with revalidateTag. Instead of invalidating paths, you could tag your cached data and invalidate by tag. One call to revalidateTag('guestbook') would invalidate the data everywhere it was used, regardless of how many pages consumed it.
This is what made caching the guestbook possible. We didn't need to know which page paths existed or how the pagination shifted. We tagged the data, and the framework handled the propagation.
But the App Router's caching model came with its own problems. fetch() requests were cached by default, which confused a lot of developers (and was therefore changed in Next.js 15 to be opt-in). The interaction between the fetch cache, the full route cache, and the router cache created a system where it was hard to reason about what was cached and what wasn't. I think the complexity of the framework's caching system and ongoing changes are still the most confusing and frustrating part of Next.js for many developers.
Cache Components: explicit and opt-in
Cache Components, enabled via cacheComponents: true in next.config.ts, are Vercel's latest answer to that confusion. The fundamental change: nothing is cached by default anymore. All dynamic code runs at request time unless you explicitly opt in with the "use cache" directive.
I wrote about why directives are the right approach for marking these boundaries. The short version: "use cache" replaced unstable_cache with a directive that can cache both data functions and entire React components, not just JSON.
import { cacheTag, cacheLife } from 'next/cache'
export async function getGuestbookEntries(page: number) {
'use cache'
cacheTag('guestbook')
cacheLife('max')
return db.guestbook.findMany({ skip: page * 20, take: 20 })
}
Arguments like page automatically become part of the cache key, so each page gets its own cache entry. When a fan posts a new entry, a call to updateTag('guestbook') ensures the next request serves fresh data immediately. (For the difference between updateTag and revalidateTag, see the revalidation article in this series.)
Cache Components also complete the story of Partial Prerendering (PPR). Before PPR, Next.js had to choose whether to render each URL statically or dynamically. PPR eliminated that dichotomy: static and dynamic content can coexist on the same page. Cache Components make this explicit. You mark what should be cached with "use cache", wrap dynamic content in <Suspense>, and the framework generates a static shell with streamed dynamic holes.
Three cache directives
Next.js provides three variants of the cache directive for different scenarios:
| Directive | Storage | Runtime APIs | Use case |
|---|---|---|---|
"use cache" | Server cache (in-memory or file-based, persists across deployments on Vercel) | Cannot access cookies/headers directly | Shared content: product listings, blog posts, guestbook entries |
"use cache: private" | Per-request cache | Can access cookies/headers | Personalized content: shopping cart, user-specific recommendations |
"use cache: remote" | Remote cache handler (Redis, KV store) | Cannot access cookies/headers directly | Rate-limited upstream APIs, shared cache across server instances |
The default "use cache" covers most scenarios. "use cache: private" (experimental) is for content that varies per user but should still be cached within a request, like personalized product recommendations based on a session cookie. "use cache: remote" is for when the default cache gets evicted too often, for example in serverless setups where memory is not shared between instances, and your upstream API can't handle the request volume.
For the full comparison including nesting rules and constraints, see the Next.js docs on cache directives.
What changed
The progression from Pages Router to Cache Components spans several Next.js versions, but the underlying shift is consistent: the framework took on more responsibility for tracking what data goes into what output. Cache Components make the opt-in explicit, replace the confusing implicit caching of the old App Router, and give you a single directive that works for data, components, and entire pages.
For the bademeister.com guestbook, the real breakthrough was revalidateTag in Next.js 13. Cache Components build on that foundation with a cleaner API and the ability to cache rendered components, not just data. Combined with PPR, the guestbook pages now load as a static shell with cached content that updates when fans post new entries.