Next.js Cache Components: what to cache and what to stream
Cache Components make caching explicit. A working rule for deciding which parts of a page get cached, which stream, and how to avoid caching a failure for an hour.
With Cache Components enabled, Next.js stops guessing. Nothing is cached unless you say so, and anything that depends on a request or on uncached data has to be wrapped in <Suspense>. That sounds strict, but it makes performance decisions visible in the code. This is the mental model I use, along with a mistake worth avoiding.
Turn it on
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
Three kinds of content
Every piece of a page falls into one of three groups:
- Static: pure functions of code and constants. It is prerendered into the static shell automatically.
- Cached: data that is expensive to compute and the same for everyone for a while. You opt in with
"use cache"and give it a lifetime. - Dynamic: depends on the request (
cookies(),headers(),searchParams) or on data that must be fresh. It streams in behind<Suspense>.
The goal is to make the static shell as large as possible, so a visitor sees most of the page immediately.
Cache the data, not the whole page
"use cache" works on functions and components. Prefer caching the data function so several components can share it:
import { cacheLife } from 'next/cache'
export async function getProducts() {
'use cache'
cacheLife('hours')
return db.query('SELECT * FROM products')
}
cacheLife accepts the built-in profiles (seconds, minutes, hours, days, weeks, max) or your own numbers. Arguments become part of the cache key, so different inputs get separate entries.
Stream what must be fresh
For data that has to be current, do not cache it. Wrap the component that reads it in <Suspense> with a fallback that holds the layout steady:
export default function Page() {
return (
<>
<h1>Dashboard</h1>
<Suspense fallback={<p>Loading…</p>}>
<LiveStats />
</Suspense>
</>
)
}
The heading is part of the static shell. Only LiveStats waits for the server. If you forget the boundary, Next.js tells you with a "blocking route" error instead of silently making the whole page slow.
The mistake: caching a failure
Imagine fetching GitHub contribution data for a portfolio. You cache it for an hour, which is sensible. Then one request fails, perhaps a rate limit or a brief outage. If the cached function returns an "unavailable" value, that value is now cached for the full hour.
Because cacheLife can be called in different branches, set a short lifetime on the failure path:
async function fetchGithub(username: string) {
'use cache'
try {
const res = await fetch('https://api.github.com/graphql', { /* ... */ })
if (!res.ok) throw new Error(`GitHub responded ${res.status}`)
const data = await res.json()
cacheLife('hours') // success: keep for an hour
return { ok: true, data }
} catch {
cacheLife('seconds') // failure: retry almost immediately
return { ok: false }
}
}
The documentation asks for only one cacheLife call to run per invocation, and this pattern respects that: each branch calls it once.
Also keep secrets and missing configuration out of the cached path. If an API token is absent, there is nothing worth caching, so return early from an uncached wrapper function and only enter the cached function when you have what you need.
Content from files counts too
Reading from the filesystem (for example, listing blog posts) is uncached I/O. Without a directive it will block prerendering. If the content only changes at deploy time, cache it for a long time:
export async function getPostSlugs() {
'use cache'
cacheLife('max')
return (await readdir(BLOG_DIR)).filter((f) => f.endsWith('.mdx'))
}
A quick decision guide
- Data that never changes between deploys: cache it with
cacheLife('max'). - Data that changes a few times a day for everyone:
"use cache"withhours. - Data that depends on the user or the request: stream it inside
<Suspense>. - Data that can fail: give the failure path a short
cacheLife.
Cache Components do not make a slow page fast on their own. What they do is force the question "can this be shared?" to be answered in code, one component at a time, which is where most performance wins come from.