# next源码主线:缓存与revalidate机制

# 为什么这篇很关键

Next 项目线上最常见的问题不是“页面报错”,而是:

  • 发布后数据没更新
  • 某些节点更新、某些节点不更新
  • 本地正常,线上缓存混乱

本质都指向一个问题:没有理解 Next 的多层缓存与失效链路。


# 先建立心智模型:Next 至少有 3 层缓存

# 1)Request Memoization(请求级去重)

同一次服务端渲染过程中,重复 fetch 相同请求可复用结果,避免 N+1。

特点:

  • 生命周期短(一次请求内)
  • 目标是“减少重复计算”,不是跨请求缓存

# 2)Data Cache(数据缓存)

fetch 配置 revalidate / tags 后,会形成跨请求可复用的数据缓存。

特点:

  • 生命周期跨请求
  • 由时间(TTL)或事件(tag/path)驱动失效

# 3)Route/Render Cache(路由渲染结果缓存)

页面或 segment 的渲染结果(含 RSC 产物)可能被缓存,减少重复 render 成本。

特点:

  • 与路由层强相关
  • 受动态 API、cookies/headers 使用影响

# 一句话

  • Request Memoization:一次请求内去重
  • Data Cache:跨请求的数据复用
  • Route Cache:跨请求的渲染复用

只要混淆这三层,排障就一定混乱。


# revalidate 到底改变了什么

当你写:

await fetch(url, { next: { revalidate: 60 } });
1

你不是“每 60 秒强制请求一次”,而是:

  • 60 秒窗口内优先返回缓存
  • 窗口到期后触发再验证流程

再验证可能是“请求触发时重建”,不是固定定时任务。


# revalidateTag 与 revalidatePath 的差异

# revalidateTag

按“数据语义”失效,适合跨页面共享数据:

  • 文章列表、作者信息、商品库存等
  • 一次失效可影响多个页面

# revalidatePath

按“路由路径”失效,适合页面级精准刷新:

  • 某个详情页路径
  • 某个列表页路径

# 实战建议

  • 业务数据驱动:优先 tag
  • 页面单点刷新:再用 path
  • 大型项目常组合使用:先 tag,再关键 path 兜底

# 失效触发链(源码理解版)

# 高层链路

  1. 写操作完成(DB/服务端成功)
  2. 调用 revalidateTag / revalidatePath
  3. Next 标记对应缓存键失效
  4. 下次命中时走重算或重拉
  5. 新结果写回缓存

关键点:失效通常是“标记 + 下次重建”,不是立即全站推送刷新。


# 常见一致性问题(生产必踩)

# 问题 1:写成功但读旧数据

常见原因:

  • 写操作后没触发 revalidate
  • revalidate 调用失败但未告警
  • 上游 CDN 仍缓存旧 HTML

# 问题 2:A 页面更新,B 页面没更新

常见原因:

  • A/B 使用了不同 tag 命名
  • 某个页面走了 path 缓存但未关联数据 tag

# 问题 3:多实例部署下表现不一致

常见原因:

  • 失效事件未正确广播(或广播延迟)
  • 各节点缓存层配置不一致

# 标签设计规范(非常重要)

推荐统一命名规则:

domain:resource:id[:scope]
1

示例:

  • cms:post:list
  • cms:post:123
  • shop:sku:9988:cn

不要用太泛的 tag(如 posts),否则失效范围不可控。


# 案例:内容平台发布链路(强一致要求)

需求:

  • 作者发布文章后,列表和详情都要快速更新
  • 不允许出现“详情新、列表旧”超过 10 秒

实现:

  1. 写库成功
  2. 触发:
    • revalidateTag("cms:post:list")
    • revalidateTag("cms:post:{id}")
    • revalidatePath("/posts")(兜底)
  3. 记录失效操作日志(含 requestId)
  4. 监控 10 秒内首个新请求是否返回新版本

核心是“失效行为可观测”,不是只靠肉眼刷新页面。


# 代码模板:发布后失效

import { revalidatePath, revalidateTag } from "next/cache";

export async function publishPost(postId: string) {
  // 1) 持久化写入(略)

  // 2) 失效数据缓存
  revalidateTag("cms:post:list");
  revalidateTag(`cms:post:${postId}`);

  // 3) 可选:关键页面路径兜底
  revalidatePath("/posts");
  revalidatePath(`/posts/${postId}`);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 调试与观测清单

上线前至少验证:

  • 冷缓存首访是否正确
  • 热缓存命中是否正确
  • 触发 revalidate 后首个请求是否拿到新数据
  • 多实例下是否一致

建议埋点字段:

  • requestId
  • cacheLayer(memo/data/route/cdn)
  • cacheHit(true/false)
  • revalidateType(tag/path/time)
  • tagOrPath

# 源码阅读路径建议(缓存主题)

建议按这条线追:

  1. fetch 包装层(如何注入 next cache 语义)
  2. 缓存 key 构建逻辑(请求参数如何归一)
  3. revalidate API 到失效标记链路
  4. 下次请求如何检测失效并重建
  5. render 缓存与 data 缓存如何协同

读源码时不要追“所有文件”,只围绕这 5 个问题。


# 常见误区

  • 误区 1:把 revalidate 当强实时机制
  • 误区 2:只配 revalidateTag 不做监控
  • 误区 3:把 CDN 缓存问题误判为 Next 缓存问题
  • 误区 4:tag 命名无规范导致误失效或漏失效

# 小结

Next 缓存机制的难点不在 API,而在“多层缓存协同”。
你可以用一句话记住主线:
先分清缓存层,再设计失效语义,最后把失效变成可观测链路。