2026-09-13 日报

Yuyang 前端小白🥬

今日主题

静态资源CDN缓存问题

BriefIntroduction

每年总有几起和 DNS / CDN 相关的线上事件😂,这次把「客户端 → 缓存 → CDN」这条链路顺手记一下。


一、常见 CDN 厂商

静态资源很少直接打源站,中间通常会过一层 CDN。国内国外常见的大概是这些(不分先后):

国内: 阿里、腾讯、百度、网宿

国外: Cloudflare、AWS CloudFront、Akamai、Fastly、Google Cloud CDN、Azure CDN


二、常见的 responseStatus

常会碰到的状态码

  • 200:完整返回资源
  • 304:协商缓存判定没变,不传 body,继续用本地
  • 206:Range 分片,音视频、大文件常见
  • 301 / 302:域名或路径跳了
  • 403:防盗链、Referer 之类拦了
  • 404:路径错了,或旧资源被清了
  • 502 / 504:边缘回源失败 / 超时
  • 503:过载、限流、维护

比较坑的:responseStatus = 0

这不是 CDN 真回了个状态码 0

监控里出现 0,常见是跨域资源没配 Timing-Allow-Origin,Resource Timing 把 responseStatustransferSizednsTimetcpTimettfb 这些敏感字段清零了;downloadtotalTime 这类耗时往往还在。也可能是请求被取消、被插件/CSP 拦了,或 no-cors 的 opaque 响应。

所以看到监控 0,别先当失败——很可能资源已经加载成功,只是字段读不到。


三、这次的问题:CDN 没把 Vary 透传下来

排查资源加载时,先撞到监控里 responseStatus = 0(上面那种 Timing 限制)。继续挖的时候发现另一类更隐蔽的问题:

本该重新请求的场景,客户端根本没发网,直接吃了 Disk Cache 里的旧内容。
Network 里经常是 (from disk cache)

Vary 是干什么的

Vary 告诉浏览器:同一个 URL,如果某些请求头不一样,缓存要分开存。

比如 Vary: Accept-Encoding——支持 gzip/br 和不支持压缩的客户端,不能共用一份缓存,不然可能把压缩包发给解不了的一方。
Vary: Origin 也是类似思路,按来源区分版本。

正常缓存 key 大致是:

1
URL + Vary 里声明的那些请求头的值

还有一点:强缓存命中时,复用的不只是 body,当时存下来的响应头也会一起复用。第一次如果缺了 Vary,后面会一直按「残缺的头 + 旧 body」用下去。

问题怎么串起来的

  1. 源站其实带了 Cache-ControlVary
  2. CDN / 网关转发时把 Vary 弄丢了,浏览器拿到的响应里没有它
  3. 浏览器觉得「这 URL 不用按请求头区分」,缓存 key 退化成只看 URL
  4. 之后请求头已经变了,本该当新版本去拉,结果只比对 URL,判定命中,直接本地复用,不发请求

因为卡在强缓存的本地判断,不联网,所以不好查。临时换个静态资源域名,等于换了一套缓存 key,现象会消失——这反而能侧面验证是客户端缓存被污染了。

结论: CDN 没原样透传 Vary → 浏览器把多个版本合成「一个 URL 一份缓存」→ 该发的请求没发出去。

排查时可以对照:源站响应头、CDN 边缘、浏览器 Network 里最终看到的头,看 Vary 还在不在。


四、客户端缓存:强缓存和协商缓存

整条链路可以先记这个顺序:

1
2
3
4
5
请求
→ Service Worker(有的话先拦,Cache Storage 是它自己的仓库)
→ 浏览器 HTTP 缓存(memory / disk,强缓存 + 协商缓存)
→ CDN
→ 源站

SW 还在用户设备上,没到 CDN。它和 HTTP 强缓存不是一套东西:SW 命中时 DevTools 会标 (from ServiceWorker);HTTP 强缓存则是 (from memory cache) / (from disk cache)

强缓存

没过期就不发请求,本地直接用。常见头:

  • Cache-Control: max-age=...(秒)
  • Expires(老写法,优先级低于 Cache-Control)
  • immutable(一般配合 hash 文件名,连过期后的校验都省)

协商缓存

强缓存过期了,或一开始就没设强缓存,浏览器才会再发一请求,问问「变了没」。不是先问再另取,一次响应里就给出结果。

两套标识,有一套就行,也可以一起带:

第一次响应里 第二次请求会带 比什么
Last-Modified If-Modified-Since 服务器上的最后修改时间
ETag If-None-Match 内容版本 / hash

Last-Modified 例子:
第一次 200,带上 Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT(服务器认为的修改时间,不是你本地时间)。浏览器把内容和这个时间一起存着。
第二次若已过强缓存期,会带 If-Modified-Since(值就是上次那个时间)。服务器对比:没变 → 304;变了 → 200 + 新文件 + 新的 Last-Modified

ETag 同理:
第一次存 ETag;第二次带 If-None-Match。相同 → 304;不同 → 200 换新。
ETag 通常比时间戳准,但不是必填。很多静态资源只靠 max-age + hash 文件名,响应里可以没有 ETag;没有 ETag 也不等于不能协商,还有 Last-Modified

304200 路径一样,差在要不要把完整 body 传回来。304 省的就是这一截带宽。

如果第二次还在强缓存期内,根本不会走到协商,直接 disk/memory 命中。

评论