四层缓存对照台
Request Memoization / Data Cache / Full Route Cache / Router Cache:四层各管一段生命周期,「时新时不新」先定位是哪一层;四层名称出自旧模型,文末附 cacheComponents 演进备注
总览:四层各管一段生命周期
从短到长排列;任何「数据没更新」的问题,答案都藏在其中一层
| 层 | 缓存什么 | 存活在哪 | 怎么失效 |
|---|---|---|---|
| Request Memoization | fetch / cache() 返回值 | 单次服务端渲染,渲染完即销毁 | 无需失效:活不过一个请求 |
| Data Cache | fetch 响应体 | 服务端,跨请求、跨部署持久 | revalidate 到期 / revalidateTag / revalidatePath |
| Full Route Cache | 整页 HTML + RSC 载荷 | 服务端,构建期/再生时产出 | revalidate 到期 / revalidatePath / 重新部署 |
| Router Cache | 访问过的路由 RSC 载荷 | 浏览器内存,随会话 | staleTimes 到期 / router.refresh() / Server Action 后自动失效 |
模型演进:四层名称出自旧模型(Next 15 及以前,route segment config 时代)。 本站已全站开启 cacheComponents:② 与 ③ 被"use cache"显式缓存模型吸收 —— 缓存什么、活多久、按什么标签失效,都声明在函数/组件上, 不再有「fetch 结果一层、整页产物一层」的隐式分层; ① Request Memoization(React cache())与 ④ Router Cache 在新模型下原样保留。 旧模型的完整四层文档见Caching without Cache Components。
「staleTimes 到期」指 Router Cache 里每份载荷的保质期:Next 15 起 page 段默认 0s(前进式导航每次都重取),布局段与预取载荷默认 30s;前进/后退还原历史页不受此限。 可用 next.config.ts 的 experimental.staleTimes 调整,语义详解见导航与 Router Cache的 staleTimes 一节。
① Request Memoization — 单次渲染内的请求去重
机制:同一渲染周期内,相同 fetch/cache() 调用只执行一次;生命周期:一个请求,渲染完即销毁
// data/blog.ts —— 本仓库正在使用的真实例子
import { cache } from "react";
export const getPosts = cache(async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
return res.json();
});
// 同一个渲染周期里:
// generateMetadata() 调一次 getPosts() ┐ 参数相同
// Page 组件再调一次 getPosts() ┘ → 只发一次 HTTP 请求
// 注意:去重只活在「单次渲染」内,跨请求不保留 —— 跨请求是
// "use cache" 缓存条目(旧模型叫 Data Cache)的职责本仓库的 /rendering/isr 页正在使用它:generateMetadata 与页面组件各调一次 getPosts(),实际只发一次请求 —— 源码见 data/blog.ts。
② Data Cache — fetch 结果的服务端持久缓存
旧模型的隐式层;本站现行(cacheComponents)下由 "use cache" 显式声明吸收,语义对应关系见代码对照
// 旧模型(Next 15 及以前):fetch 默认不缓存,
// 显式进入 Data Cache 靠 fetch 选项
const cached = await fetch("https://api.example.com/posts", {
next: { revalidate: 60, tags: ["posts"] },
});
// 本站现行(cacheComponents):取数默认动态,
// 缓存用 "use cache" 声明在函数/组件上,粒度不再绑死 fetch
async function getPosts() {
"use cache";
cacheLife({ revalidate: 60 }); // 对齐旧 next.revalidate
cacheTag("posts"); // 对齐旧 next.tags
const res = await fetch("https://api.example.com/posts");
return res.json();
}
// 失效方式(两模型一致):
// - 时间到:60s 后下一个请求触发后台重取(陈旧-回源-再验证)
// - 主动:revalidateTag("posts") 按标签精准失效
// revalidatePath("/data/cache-layers") 连带该路径的缓存产物一起失效③ Full Route Cache — 整页静态产物
旧模型的隐式层;本站现行(cacheComponents)下由预渲染 + "use cache" 产物承接,○/◐ 标记即其形态
- 进入该缓存的样子:/rendering/isr—— 取数标 "use cache" + cacheLife({ revalidate: 60 }), 产物 60s 后由下一个请求触发后台再生(build 输出 ○,Revalidate 列 1m)
- 壳进缓存、洞不进的样子:/rendering/streaming—— ◐(Partial Prerender):静态壳预渲染,Suspense 洞每次请求运行时重渲
- 本页自己也是素材:无动态 API、无请求时数据,生产构建后被整体预渲染; build 输出里 ○(静态)标记即代表产物可被长期缓存
④ Router Cache — 客户端导航缓存(可运行)
机制:访问过的路由 RSC 载荷存在浏览器内存;生命周期:会话内,过期或手动 refresh
操作路径:点上面任一链接离开,再按浏览器「后退」回来 —— 打开 DevTools Network 对照:后退命中 Router Cache,不产生新的文档/RSC 请求; 点 refresh 按钮则会立刻看到一次新的 RSC 载荷请求。
什么时候别缓存 & 排查入口
缓存省的是重复,花的是新鲜度;先想清楚数据变脏了谁受影响
- 会话相关数据别进共享缓存:购物车、个人中心这类按人不同的数据, 用 cookies()/headers() 让内容在请求时渲染(自动成为动态洞),或下沉为客户端取数(SWR)
- 强实时数据别缓存:行情、库存、计数器,缓存带来的省钱抵不上脏数据的代价
- 「时新时不新」排查顺序:① 先退 Router Cache(硬刷新绕过客户端缓存)—— 好了就是客户端层;② 再看整页产物(路由是否被预渲染,build 输出是 ○ / ◐ 还是 ƒ)—— 静态页摘掉 "use cache" 或改 connection() 验证;③ 最后查取数层(缓存条目的 cacheLife/cacheTag 是否如预期,源站其实早就变了)—— 一层一层往下剥,而不是全局禁用缓存