四层缓存对照台

Request Memoization / Data Cache / Full Route Cache / Router Cache:四层各管一段生命周期,「时新时不新」先定位是哪一层;四层名称出自旧模型,文末附 cacheComponents 演进备注

总览:四层各管一段生命周期

从短到长排列;任何「数据没更新」的问题,答案都藏在其中一层

层缓存什么存活在哪怎么失效
Request Memoizationfetch / cache() 返回值单次服务端渲染,渲染完即销毁无需失效:活不过一个请求
Data Cachefetch 响应体服务端,跨请求、跨部署持久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" 产物承接,○/◐ 标记即其形态

注意:整页静态产物只在生产构建(next build + next start)下生效。next dev 里每个请求都重新渲染,观察不到这一层; 而下面第 ④ 层 Router Cache 在 dev / prod 行为一致,可以直接在 dev 里演示。
  • 进入该缓存的样子:/rendering/isr—— 取数标 "use cache" + cacheLife({ revalidate: 60 }), 产物 60s 后由下一个请求触发后台再生(build 输出 ○,Revalidate 列 1m)
  • 壳进缓存、洞不进的样子:/rendering/streaming—— ◐(Partial Prerender):静态壳预渲染,Suspense 洞每次请求运行时重渲
  • 本页自己也是素材:无动态 API、无请求时数据,生产构建后被整体预渲染; build 输出里 ○(静态)标记即代表产物可被长期缓存

④ Router Cache — 客户端导航缓存(可运行)

机制:访问过的路由 RSC 载荷存在浏览器内存;生命周期:会话内,过期或手动 refresh

去 Route Handler 页 →去 ISR 页 →

操作路径:点上面任一链接离开,再按浏览器「后退」回来 —— 打开 DevTools Network 对照:后退命中 Router Cache,不产生新的文档/RSC 请求; 点 refresh 按钮则会立刻看到一次新的 RSC 载荷请求。

什么时候别缓存 & 排查入口

缓存省的是重复,花的是新鲜度;先想清楚数据变脏了谁受影响

  • 会话相关数据别进共享缓存:购物车、个人中心这类按人不同的数据, 用 cookies()/headers() 让内容在请求时渲染(自动成为动态洞),或下沉为客户端取数(SWR)
  • 强实时数据别缓存:行情、库存、计数器,缓存带来的省钱抵不上脏数据的代价
  • 「时新时不新」排查顺序:① 先退 Router Cache(硬刷新绕过客户端缓存)—— 好了就是客户端层;② 再看整页产物(路由是否被预渲染,build 输出是 ○ / ◐ 还是 ƒ)—— 静态页摘掉 "use cache" 或改 connection() 验证;③ 最后查取数层(缓存条目的 cacheLife/cacheTag 是否如预期,源站其实早就变了)—— 一层一层往下剥,而不是全局禁用缓存