HTTP 与缓存
为什么前端工程师必须把缓存当成协议问题来学
很多人第一次学缓存,是从“资源第二次加载更快”开始的。
这个入口没有问题。
但如果只停在这里,后面就会越来越混乱。
因为真实系统里的缓存,不只是浏览器里的一个开关。
它同时牵涉:
- HTTP 响应头
- 浏览器缓存
- CDN 和代理缓存
- 资源版本控制
- 动态数据的新鲜度
- revalidation
所以更稳妥的理解是:
缓存首先是协议语义问题,然后才是性能收益问题。
先把“缓存”讲准确
缓存可以先理解成:
在未来可能重复使用的数据上,提前保存一个可复用副本,以减少重复获取成本。
但这句话还不够。
在 Web 里,更关键的问题是:
- 谁在缓存
- 缓存多久
- 是否允许复用
- 什么时候必须重新验证
这几个问题,才真正决定缓存行为。
HTTP 缓存到底在解决什么
它解决的不是“快一点”这么简单。
更完整地说,它在平衡三件事:
- 响应速度
- 网络与服务器成本
- 数据新鲜度
如果只追求快,缓存可能会过期太久。
如果只追求最新,缓存收益又会非常低。
所以 HTTP 缓存本质上是一个受约束的交换。
浏览器缓存、共享缓存、应用数据缓存不是一回事
这是很常见的混淆点。
浏览器缓存
终端用户浏览器本地保存的响应副本。
共享缓存
通常是 CDN、反向代理或中间缓存节点持有的副本。
应用数据缓存
比如 React Query、SWR 或业务层自己维护的数据缓存。
这三种缓存可能同时存在。
如果不区分层次,讨论很容易乱掉。
Cache-Control 为什么是最核心的入口
因为它直接告诉缓存系统:
这个响应该怎么被缓存和复用。
这不是唯一相关头部。
但通常是最关键的一组指令。
max-age 可以怎么理解
最简单的理解是:
在这个时间窗口内,缓存副本可以被视为新鲜的。
这里最重要的词是“新鲜”。
因为缓存并不是只有“有”或“没有”。
它还有“新鲜”和“陈旧”的状态差别。
什么叫 freshness
freshness 就是:
一个缓存响应是否还能直接使用,而不先去问源站“它还有效吗”。
如果一个响应仍然 fresh,浏览器或中间缓存通常可以直接复用它。
如果它 stale 了,就要进入 revalidation 逻辑。
revalidation 是什么
revalidation 可以简单理解成:
缓存副本虽然还在,但不再被直接信任,于是客户端带着某些校验信息去向源站确认它是否还能继续复用。
这是 HTTP 缓存里非常关键的一层。
因为很多系统真正高效的地方,不是完全不请求。
而是发出成本更低的验证请求。
ETag 和 Last-Modified 应该怎么讲
它们都属于 validator。
也就是缓存验证器。
ETag
服务端给资源生成的标识。
客户端下次请求时可以带上 If-None-Match。
如果服务端确认内容没变,就返回 304 Not Modified。
Last-Modified
表示资源最后修改时间。
客户端可以带 If-Modified-Since 去验证。
和 ETag 比,它通常更粗粒度。
304 Not Modified 为什么重要
因为它体现了 HTTP 缓存真正的聪明之处:
不是简单地“要么全下载,要么不下载”。
而是允许你在确认资源没变时,复用已有响应体。
这能同时兼顾:
- 新鲜度
- 带宽
- 响应效率
no-cache 经常被误解成“不缓存”
这是前端里非常高频的误解。
更准确地说,no-cache 通常不是“绝对不存”。
它更接近:
允许缓存存下来,但在复用前必须重新验证。
真正更接近“不要存”的是:
no-store
这两个不能混着讲。
no-store 为什么更严格
它强调的是:
不要把响应存进任何缓存。
这通常更适合:
- 高敏感信息
- 不应被本地或中间层持久化的响应
所以如果你需要表达“绝对不该保留”,更应该想到 no-store,不是 no-cache。
public 和 private 为什么重要
因为不是所有缓存都只在浏览器本地发生。
public 允许共享缓存复用。
private 则更偏向仅终端用户私有缓存可用。
这在登录态页面、个性化资源和 CDN 配置里尤其关键。
为什么静态资源和接口数据的缓存策略经常不同
因为它们的变化模式和业务风险不同。
静态资源
常见策略是:
- 文件名带 hash
- 长时间缓存
- 内容变了就换 URL
这样缓存命中高,失效逻辑也简单。
接口数据
通常更强调:
- 新鲜度
- 条件验证
- 权限和个性化
- 局部更新
所以接口缓存更像一套动态平衡,而不是一把锁死的长期缓存。
为什么“文件名带 hash”这么常见
因为这是一种非常实用的缓存失效策略。
资源内容一变,URL 跟着变。
这样老缓存就不会误命中新内容。
对于 JS、CSS、图片这类静态资源,它比“强行缩短缓存时间”通常更高效。
CDN 缓存为什么不能和浏览器缓存混为一谈
CDN 处在共享缓存层。
它面对的是:
- 多用户
- 多地区
- 多边缘节点
所以它的缓存控制不仅影响单个用户体验,也影响整体分发成本。
同一个 Cache-Control,在浏览器和 CDN 上可能都生效,但讨论语境不一样。
为什么缓存总会和个性化、鉴权冲突
因为缓存喜欢“大家都能复用的响应”。
而个性化和鉴权经常意味着:
- 响应因用户而异
- 某些内容不应该被共享缓存复用
所以一旦响应和登录态、权限、地区、实验分流绑定,缓存策略就必须更谨慎。
Vary 为什么值得知道
Vary 可以理解成:
告诉缓存系统,响应不仅由 URL 决定,还受某些请求头影响。
这在内容协商、压缩或语言版本里很重要。
如果该 Vary 的地方没写,缓存可能会错把 A 用户的变体给到 B 用户。
前端为什么不能只把缓存交给后端和 CDN 团队
因为前端直接影响:
- 资源命名策略
- 构建产物 hash
- 请求方式
- 页面更新时机
- 本地数据缓存策略
如果前端完全不理解 HTTP 缓存,就很容易出现:
- 刚发版用户看不到新资源
- 某些接口一直读到旧数据
- 本地数据缓存和网络缓存互相打架
HTTP 缓存和 React Query 这类数据缓存是什么关系
两者不是替代关系。
HTTP 缓存更偏协议层。
React Query 这类工具更偏应用层数据生命周期管理。
它们可以叠加。
但不应该混成一个概念。
更成熟的讲法是:
- HTTP 缓存控制网络响应复用
- 应用层缓存控制组件消费和状态同步
stale-while-revalidate 为什么近几年经常被提到
因为它很好地体现了现代缓存策略的一种思路:
允许先用一个可能略旧但仍可接受的结果,同时在后台更新。
这在体验上很有吸引力。
但它也意味着团队必须接受:
某些时刻用户看到的不是绝对最新值。
所以它不是“总是更好”。
它是一种明确 trade-off。
常见误区
1. 把 no-cache 说成“不要缓存”
这是经典误区。
2. 以为缓存只发生在浏览器
现实里共享缓存和边缘缓存非常重要。
3. 以为缓存命中越高越好
命中率高不一定代表策略正确。
如果旧数据风险高,命中率高也可能是问题。
4. 把静态资源缓存和接口缓存用同一套直觉处理
这通常会出事。
中国互联网语境下的常见缓存难点
比较常见的是:
- 多 CDN、多网关、灰度链路并存
- 活动页和交易页节奏都很快
- 资源发版频繁
- WebView 和浏览器环境并存
这会让缓存问题更容易表现成:
- 部分用户拿到旧资源
- 某些环境刷新后行为不一致
- CDN 和浏览器缓存交织导致排障困难
海外产品语境里,为什么更常把缓存和协议语义放在一起讲
很多海外文档和工程文章会更强调:
- HTTP 语义
- shared cache
- validator
- stale 策略
这对前端是好事。
因为它会逼你从平台能力而不是“经验偏方”去理解缓存。
这类主题为什么很适合做表达训练
因为它太容易被讲成:
- 缓存可以让页面更快
这不够。
更好的表达应该继续说明:
- 缓存是在速度和新鲜度之间做平衡
- freshness 和 revalidation 是关键概念
no-cache和no-store不能混讲- 静态资源和接口数据策略通常不同
建议实践
实践 1:做一次资源缓存头实验
练什么:
理解 Cache-Control 与资源更新的关系。
最小交付物:
一个带 hash 资源和不同缓存头的最小 demo。
验收标准:
- 能观察到首次请求、命中缓存和重新验证的差异
- 能解释为什么改文件名能改变缓存行为
常见误区:
- 只刷新页面看快不快,不看响应头和状态码
实践 2:抓包观察 ETag / 304
练什么:
建立 validator 和 revalidation 直觉。
最小交付物:
一次浏览器 Network 面板记录。
验收标准:
- 能解释
If-None-Match与304的作用 - 能区分“没请求”与“发起了验证请求”
常见误区:
- 把
304误解成“完全没走网络”
实践 3:对比静态资源和接口缓存
练什么:
理解不同对象的缓存策略为什么不同。
最小交付物:
一份静态资源与 API 响应缓存策略对照表。
验收标准:
- 能说明各自重点约束
- 能说明为什么不能一刀切
常见误区:
- 只拿“越久越好”做判断