改代码页面不更新?强缓存和协商缓存 6 次请求实测 Cache-Control

12 阅读9分钟

同一个 /index.html,我在服务器上把内容改了一版再打开,浏览器 2.4 毫秒、0 字节就把它端了出来,改过的那一版根本没露面。

后来我把这类访问串成 6 次请求跑了一遍:第 3 次服务器只回 304,一共 624 字节;6 次加起来 71.5 KB,比「每次都全量下载」省了 66%。

图 1:服务器、Cache-Control 开关、请求时间线和判定流程四块在同一屏,右侧每一行是一条真实请求

在线案例与源码:atomgit.com/deli007/dem… 成品只有一个 index.html,双击就能打开。下面的数字都是我在页面里跑出来、从时间线行里读回来的。

一、为什么值得搞清楚:不是缓存坏了,是它在按规则办事

结论先放这:改了代码页面不更新,基本不是「缓存坏了」,而是浏览器在老老实实执行你服务器给的规则。

规则只有三条路。

强缓存还没过期,就不发任何网络请求,直接读本地磁盘,0 字节。

强缓存过期了,就发一次请求,但带上协商头,服务器回 304,只传响应头。

要么没得商量,no-store 或者缓存里根本没存验证器,那就每次全量下载。

我第一次踩这个坑是本地调试:改了 index.html 刷新还是老样子,按 Ctrl+F5 才变。

当时以为是浏览器耍赖,其实是 max-age 还没走完,浏览器压根不认为需要问服务器。

图 2:页面刚打开,服务器是 v1、内容 30.7 KB、ETag「v1-17e9」,时间线还空着

二、准备环境:进入码道 Web,把「要能自己算数」写进需求

浏览器打开码道 Web 版:devcloud.cn-north-4.huaweicloud.com/chat?source… ,登录后直接在对话框写需求,就能拿到一个能跑的单文件应用,不用装软件。

码道有三种使用方式:WebUI(浏览器对话)、TUI(终端命令行)和桌面 IDE(IDE 插件)。本文用 WebUI 版演示。

我那轮需求里最值钱的是四条。

  • 页面里的字节数必须按公式算出来,不许写死:请求头固定 220 字节,200 的响应头基数 470、304 的基数 300,ETag 占 46、Last-Modified 占 58。
  • 内容大小也要有公式:body = 24000 + 版本号 × 5500 + 尾数,这样改一次版本号,体积和 ETag 都跟着变。
  • 要能一键跑完整序列:首次访问、立刻再访问、过期后访问、服务器更新后访问,一路跑完。
  • 失败路径要有明确的红色警示,不能只改一行小字。

最后一条是这次的关键,我要看的不是正常流程多顺,而是「服务器已经更新、用户还在看旧页面」的那一下。

三、一屏看清:左边规则、中间操作、右边时间线

页面分四块,先说清楚哪块是干什么的。

左上角是服务器状态:资源路径、文件版本、内容大小、ETag、Last-Modified。

左中是 Cache-Control 配置:max-age 可以填数字,也可以点 10s / 60s / 1h / 0s 预设。

下面五个开关:no-store、no-cache、must-revalidate、下发 ETag、下发 Last-Modified。

中间是六个操作按钮,最后两个是一键跑完整序列和重置实验。

右边是请求时间线瀑布图,每一行给序号、时刻、结果状态、版本、传输字节、耗时、判定原因。

我最喜欢的一栏是最右边的「判定原因」,它把浏览器心里那句话直接写出来了。

比如「max-age 未过期(剩 10s)→ 直接读本地磁盘缓存,0 网络字节」。

四、实测:6 次请求逐条读数

默认配置(max-age=10,ETag 和 Last-Modified 都开),点一次「一键跑 6 次请求」,我这次读到的全表如下。

序号时刻结果版本传输字节耗时判定原因
01T+0s200 全量下载v131.5 KB504.7 ms无本地缓存,全量下载并写入缓存
02T+0s200 from disk cachev10 B2.4 msmax-age 未过期,剩 10s,0 网络字节
03T+10s304 Not Modifiedv1624 B45.5 ms过期后协商,ETag 匹配,复用本地 30.7 KB body
04T+20s200 全量下载v238.8 KB444.5 ms协商时验证器变了,拉新版本 v2
05T+20s200 from disk cachev20 B3.8 ms新缓存未过期,剩 10s
06T+30s304 Not Modifiedv2624 B94.7 msETag v2 匹配,复用本地 38.0 KB body

关键的一行是第 03 行,服务器只回了 624 字节。

同一次请求如果全量下载,是 32267 字节,624 只有它的 1.9%。

页面底部的统计面板给的是:命中 4 / 共 6 次,命中率 67%,6 次总传输 71.5 KB,相比每次都全量下载省下 139.4 KB、约 66%。

图 3:跑完整序列,时间线里强缓存命中、协商 304、更新后全量拉取三种状态同时出现

耗时也是分层的:全量在 394509 毫秒,304 在 4595 毫秒,磁盘命中在 1.9~4.6 毫秒。

磁盘命中比 304 还快一个数量级,因为它连请求都没发出去。

五、三组开关对照:关掉验证器最要命

同一个页面,我只改开关再跑,拿到三组差异很大的结果。

配置我实测到的结果
默认,max-age=10,ETag 加 Last-Modified6 次命中 4 次,命中率 67%,省 66%
关掉 ETag 和 Last-Modified过期后没法 304,全量 200:31.4 KB、471.5 ms
打开 no-store连续两次都是全量 200,394.5 / 508.9 ms,命中率 0%
max-age=3600 加服务器更新2.2 ms 读到旧页面,全程不联网

第二行是容易忽略的:max-age 配得好好的,但缓存里没有验证器,过期请求只能全量重来。

页面里那句话的原文是「缓存内无 ETag / Last-Modified 验证器 → 无验证器无法 304,全量 200 重新下载」。

字节上也对得上:带两个验证器的全量是 32267 字节,两个都不带是 32163 字节,差 104 字节,正好是 46 加 58。

也就是说,验证器本身几乎不要钱,省下的却是整个 body。

图 4:打开 no-store 后连续两次访问,都是全量 200,缓存栏一直是空的

六、最坑的一种:max-age 给太大,用户一直看旧页面

把 max-age 改成 3600 再点「服务器内容已更新后访问」,页面依旧不报错。

它只在顶部挂一条横幅,时间线上多一行红字:

⚠ 用户看到的仍是旧页面(缓存未失效)— 服务器已更新到 v2,本地强缓存的 v1 还剩 3600s 才过期,期间不会发起任何网络请求。

这一行的耗时是 2.2 毫秒,传输 0 字节。

对用户来说这不算 bug,页面秒开;对发布来说这就是事故,你改了版本,不刷新的用户永远看不到。

所以 max-age 的真正语义不是「缓存多久」,而是「允许用户看多久旧内容」。

静态资源敢给 3600 甚至 31536000,是因为文件名里带了版本号或哈希,内容一变 URL 就变。

HTML 这类入口文件给这么长,等于给自己埋雷。

图 5:服务器已经是 v3,缓存里还是 v2,红色那行就是「旧页面 from disk cache」

七、验证与踩坑:我把每个开关都扳了一遍

我把这个页面当成判定路径的尺子用,逐条核对。

max-age、ETag、Last-Modified 三个开关两两组合跑下来,结果和 HTTP 缓存的判定顺序一致:先看 no-store,再看有没有本地缓存,再看新鲜度,最后才谈协商。

no-store 的优先级确实最高。Cache-Control: public, max-age=3600, no-store 同时挂上,命中率还是 0%。

我特意看了一眼 must-revalidate:在这个页面里它只改响应头,判定路径上没有对应分支。

原因也说得通,这个 demo 的规则本来就是「过期一定去协商」,不会拿过期内容兜底,所以这个开关体现不出差别。

真实浏览器里它管的是另一件事:过期之后禁止用旧内容凑合。

失败路径(服务器更新、强缓存却没过期)确实有两处提示:顶部横幅和时间线红行,关掉横幅还剩红行,不会静默。

边界也写清楚:这是一个规则模拟器。

传输字节是按公式真算的,耗时不是,它是页面用「180~320 毫秒随机基数 + body/150」算出来的,不是真网络往返。

所以它适合核对判定路径,不适合当带宽测试。

八、本地复现与下一步

想自己跑一遍,两条路。

直接下载 index.html 双击打开,或者克隆公开仓库:

git clone https://atomgit.com/deli007/demo_park.git
cd demo_park\codearts-http-cache-lab
python -m http.server 8080

然后浏览器打开 http://localhost:8080/index.html。

用 http:// 打开更接近真实站点,file:// 下有些浏览器行为会对不上。

下一步我打算做两件事:把 max-age 和文件名哈希配在一起,看长缓存到底还剩多少全量下载;再拿 CDN 的主动刷新和被动过期做一次对照。

九、使用码道体会

我这次没让它做一个好看的缓存示意图,而是让它做一个能自己算数的页面。

需求里那四条是这次最省事的地方:数字进公式不进文案,我写文章时不用另抄一遍;一键完整序列,我读数和截图一次拿全。

真正需要我补一轮的是失败路径的提示位置,第一版只有一行小字,我要求做成顶部横幅。这种事按标准在操作区报错没用,得让用户一眼看到。

码道 Web 这次交付里最让我省心的,是它把「判定原因」这种中间状态显示了出来。这类页面光看结果看不出对错,能看到原因才能自查。

顺便说一句,这类模拟器都别拿来当性能结论用,它的数字是规则的示范,不是网络的真相。