Next.js cacheLife
Next.js Cache Components 用 cacheLife 描述缓存的 stale、revalidate、expire 三阶段生命周期,分别控制客户端直接复用、服务端后台刷新与强制重新生成。
[!info] related notes
- 所属 MOC:
- 前置概念: nextjs-cache-components
- 并列概念: nextjs-tag-based-revalidation
- 易混淆概念: tanstack-query-gctime-vs-staletime
- 关系笔记: nextjs-cache-vs-tanstack-query、cache-strategies-and-consistency
Next.js cacheLife
一句话定义
cacheLife() 用于 Next.js Cache Components 的 "use cache" 作用域,描述缓存结果在时间轴上的三个不同阶段:
stale
→ 客户端多久可以直接复用已有结果
revalidate
→ 多久后下一次访问应触发服务端后台刷新
expire
→ 旧结果最长可以存活到什么时候,之后必须重新生成
它解决的不是“要不要缓存”,而是:已经决定缓存以后,这份 projection 可以多旧、什么时候后台更新、什么时候必须 fresh。
基本写法
import { cacheLife } from "next/cache"
export async function getPosts() {
"use cache"
cacheLife("hours")
return postRepository.list()
}
也可以自定义:
cacheLife({
stale: 300,
revalidate: 3600,
expire: 86400,
})
Next.js 还提供 seconds、minutes、hours、days、weeks、max 等 profile,目的是让代码表达业务 freshness,而不是到处散落魔法数字。
stale:客户端直接复用窗口
在当前 Cache Components 模型中,stale 主要描述 Client Router Cache freshness window:
已有 route/cache result
↓
还在 stale window 内
↓
客户端可以直接复用
因此:
stale
≠ HTTP Cache-Control max-age
不要把它直接理解为浏览器普通 HTTP 缓存头。
一个实用直觉:
stale回答“客户端这段时间内要不要重新向服务器确认”。
revalidate:先旧后新的后台刷新
超过 revalidate 周期以后,下一次请求可以:
Request
↓
已有缓存
↓
先返回旧结果
↓
后台重新计算 / 读取
↓
更新缓存
这是一种 stale-while-revalidate 风格:
low latency
+
eventual freshness
所以 revalidate 很适合:
- 公开文章
- 产品目录
- CMS 内容
- 允许短暂 stale 的聚合数据
expire:旧结果的硬上限
expire 表示服务器缓存不能无限依赖旧结果。
当缓存超过允许的最大生命周期,需要:
Request
↓
old cache no longer acceptable
↓
重新生成 fresh result
↓
再返回
因此最短区别是:
revalidate
= 可以先旧后新
expire
= 必须 fresh 后再继续
三段时间轴
用概念时间轴理解:
0 ───────── stale ───────── revalidate ───────── expire
│
├─ 客户端可直接复用
│
├────────── 到 revalidate 后:可先旧、后台刷新
│
└────────────────────────── 到 expire 后:必须重新生成
不要只背数字,要记住三者解决的是三个不同问题。
cacheLife 与业务语义
Digital Biome 公开文章
更新很少
所有用户大体相同
几十秒甚至更长 stale 可以接受
适合较长 profile,例如:
cacheLife("days")
// 或按实际发布节奏选择更长 profile
如果内容系统能主动通知更新,甚至可以用长缓存 + tag revalidation,而不是频繁时间刷新。
Product Catalog
访问量大
更新频率中等
短暂 stale 可接受
可以使用中等 cacheLife,并把真正的产品更新事件交给 Tag-based Revalidation。
Agent Run
seq / status / events
高频变化
freshness 要求高
此时问题不是“选 seconds 还是 minutes 更好”,而可能是根本不应该进入长缓存:
fresh snapshot
+
SSE
Time-based Revalidation 的边界
时间策略的优点:
简单
不依赖业务事件
自动恢复 freshness
缺点:
即使数据没变化,也可能重新计算
数据刚变化,也可能要等时间窗口
因此,如果系统明确知道:
“数据刚刚变了”
通常更适合 on-demand invalidation,而不是只等 cacheLife 时间过去。
与 TanStack Query staleTime 的区别
两者都出现“stale”这个词,但不要一一对号入座。
TanStack Query:
staleTime
→ 一份浏览器 Query projection 多久被视为 fresh
Next.js cacheLife:
stale
→ Client Router 对缓存 route/result 的直接复用窗口
revalidate
→ Server Cache 的后台刷新节奏
expire
→ Server Cache 的硬上限
因此:
Next cacheLife.stale
≠ TanStack Query staleTime 的同一个缓存层
Next cacheLife.expire
≠ TanStack Query gcTime
详细关系见 Next.js Cache 与 TanStack Query 的关系。
缓存时间不是正确性的替代品
错误做法:
“用户数据应该新一点”
→ 随便设 10 秒
真正需要先问:
这份数据允许多旧?
用户刚写入后能否看到旧值?
有没有业务事件能主动 invalidate?
多旧会产生什么风险?
例如余额、权限、医疗结果和正在执行的 Agent 状态,对 stale 的容忍度完全不同。
最短记忆
cacheLife
= 缓存时间语义
stale
= 客户端直接复用窗口
revalidate
= 先旧后新,后台刷新
expire
= 旧值硬上限,必须重新生成