Caching: 'use cache', tags and PPR
Cache Components, cacheLife, cacheTag, updateTag vs revalidateTag.
Updated
What a cache is
A cache = you keep the result of an expensive operation (a DB query, an API request, a render) so you don't redo it on every request. The result: instant pages and less load on the server.
The price: cached data can go stale. That's why every cache needs an invalidation strategy — when to throw away the old copy.
Cache Components (Next 16)
The model in Next 16, enabled in next.config.ts:
const nextConfig = { cacheComponents: true, partialPrefetching: true }The rule: nothing is cached by default. You explicitly mark what gets cached, with the 'use cache' directive.
'use cache'
import { cacheLife, cacheTag } from 'next/cache'
export async function getPosts() {
'use cache'
cacheLife('hours') // how long it lives
cacheTag('posts') // a tag for invalidation
return db.post.findMany()
}- The function's arguments automatically become part of the cache key:
getPost(1)andgetPost(2)are separate entries. - It goes on data functions, on components or on a whole page.
- You can't read
cookies()/headers()inside it — you read them outside and pass the value as an argument.
The cacheLife profiles
| Profile | Revalidation | Expiry | For |
|---|---|---|---|
seconds |
1s | 1 min | near-live (becomes dynamic) |
minutes |
1 min | 1 h | feeds, prices |
hours |
1 h | 1 day | editorial content (the lessons here) |
days |
1 day | 1 week | rarely changing pages |
max |
30 days | 1 year | almost static |
What Partial Prerendering is
PPR combines static and dynamic in the same page:
- At build time, Next renders everything it can (including what's in
'use cache') → a static shell, served instantly from a CDN. - The dynamic parts (in
<Suspense>, reading cookies etc.) are rendered at request time and streamed.
Exactly this app: the lesson is in the static shell; your progress (from a cookie) is streamed in.
The output of npm run build shows each route's type: ○ static, ◐ partial prerender, ƒ dynamic.
Invalidation — kinds and when
| Function | Where | Behavior | When |
|---|---|---|---|
updateTag('posts') |
only Server Actions | expires immediately; the next request waits for new data | the user changed something and must see it right away (read-your-own-writes) |
revalidateTag('posts', 'max') |
Server Actions + Route Handlers | stale-while-revalidate: serves the old one, refreshes in the background | content from a CMS / a webhook — a small delay is fine |
revalidatePath('/blog') |
the same | invalidates a whole route | you don't have suitable tags |
refresh() |
Server Actions | refetches the current page's dynamic data | after mutations without a cache tag (like saving progress here) |
Summary
- Nothing is cached by default;
'use cache'+cacheLife+cacheTag, explicitly. - PPR = an instant static shell + dynamic parts streamed in.
updateTagfor the user who just made a change;revalidateTag(tag, 'max')for everything else.