假设图书馆网站修改了今天的开放时间,但部分访客仍看到旧时间。应先区分:服务器上的原始内容是否尚未修改、浏览器是否复用了已存响应,还是中间服务器提供了旧版本。多次刷新页面这一事实,并不能说明问题发生在哪个环节。
HTTP 缓存会存储收到的响应,并在满足条件时再次使用。本文用公开开放时间表这一虚构案例,将 Cache-Control 的存储、重新验证指令与 ETag、304 响应联系起来。下面的通信记录是为理解原理而简化的示例,并非对真实图书馆或特定服务器的测量。目标是解释哪些响应可以在何时复用,而不是直接开出清除一切缓存的处方。
1. 分开考虑是否存储、是否验证以及与谁共享
阅读缓存策略时,分别写下三个问题:是否允许存储响应、下次请求能否直接使用存储副本、能否在多个用户之间共享。允许存储,不代表总能复用;检查是否最新,也不代表可以与其他用户共享。HTTP 缓存标准 RFC 9111用不同指令规定这些条件。以下讨论的是没有另外附加字段名参数的响应指令。
| 要判断的问题 | 相关响应指令 | 需要区分的含义 |
|---|---|---|
| 是否不应存储响应 | no-store | 禁止为日后复用而存储 |
| 再次使用前是否需要确认 | no-cache | 可以存储,但复用前必须成功验证 |
| 能否在多个用户之间共享 | private | 排除共享缓存的存储,但并不禁止私有缓存存储 |
例如,公开时间表在同一地址频繁修改时,可以考虑要求在使用存储副本前先确认。个人页面与公开时间表,首先就有不同的共享条件。private 是针对共享缓存的指令,不能代替登录和权限检查。把目的不同的设置混为一句话,可能在改善速度时弄错信息的接收对象。
策略表中除了指令,还应写明原因。例如,时间表是不是需要检查更新的公开信息、标志图片是不是内容固定的文件、同一地址是否包含个人信息。这张表是分类实际响应的起点,而不是适用于所有网站的完整配置。还应区分它针对浏览器、CDN 等哪一种存储位置。
2. 不要仅凭名称选择 no-cache 和 no-store
no-cache 容易被理解成禁止存储,但响应中的 no-cache 表示在复用存储副本前必须验证。服务器确认内容没有变化后,就有机会继续使用已经保存的正文。相比之下,no-store 指示不要存储本次响应。存不存与下次检查什么是不同的问题,不能预期这两个设置产生相同的行为。
缓存中的 fresh 指按策略可以不经额外验证就再次使用的状态,并不保证运营者在这段时间内没有修改内容。若 max-age 规定了允许复用的期限,那么即使原始内容在期限内改动,已存响应仍可能被选中。应先确定可以接受的更新延迟,再将要求与指令联系起来。不要把任意的长期保存时间当作性能优化的标准答案照搬。
MDN 的 HTTP 缓存指南说明,在新响应中加入 no-store,并不会删除同一 URL 已存的旧响应。如果旧副本仍然有效,客户端尚未去获取新响应,新标头也可能尚未传达。不要把 no-store 看成清除全部缓存或保障隐私的充分条件;还应检查旧响应的寿命以及可管理缓存的更新程序。
3. 用 ETag 和 304 跟踪一次再次访问
假设虚构时间表地址 /hours.json 最初表示 18 时闭馆。下面使用说明性的 HTTP/1.1 格式,仅保留必要部分;真实响应所需的其他标头已省略。hours-v1 是示例中由服务器确定的表示标识符,不是计算时间的公式,也不表示真实文件的哈希值。
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-cache
ETag: "hours-v1"
{"closes":"18:00"}
如果浏览器保存了该响应,下次可以在条件请求的 If-None-Match 中发送 ETag 值。服务器判断它与当前选定表示的条件相符、内容未变化时,就返回 304 Not Modified。下面将请求及对应响应连续写成简化记录,并不是在 304 后另行发送新的时间表 JSON。
GET /hours.json HTTP/1.1
Host: library.example
If-None-Match: "hours-v1"
HTTP/1.1 304 Not Modified
Cache-Control: no-cache
ETag: "hours-v1"
正如 MDN 的 ETag 说明所述,此时客户端会使用已存正文。304 没有新正文,但仍有发出请求和验证所需的通信。如果运营者把闭馆时间改为 19 时,服务器的表示也已变化,那么面对旧条件,可以预期返回包含新正文和新标识符的 200 响应。使用 no-cache 不等于每次都得到 304,也不等于不再查询数据库。
比单看状态码更重要的是正文与标识符的对应关系。内容已经变化,服务器却仍使用旧 ETag,可能得出错误的验证结果;反之,内容相同而标识符不断变化,则可能增加重传正文的次数。需要在服务器端确认标识符生成方式与选定表示的关系,不能仅因出现 ETag 就认定实现正确。
4. 区分经常变化的文档与使用新名称的资源
像时间表这样内容在同一 URL 改变的文档,与内容变化时就分配新文件名的标志图片、脚本,可以采取不同的更新策略。例如,若设计为将 logo.a1.png 换成 logo.b2.png,就需要让新文档指向新地址。若文件名包含标识符,却仍覆盖旧文件内容,就破坏了同时管理内容和地址的约定。
上传新文件本身,并不会使访客请求新地址。如果访客收到的 HTML 仍指向旧标志地址,就可能再次使用旧资源。因此,除了资源的缓存设置,还应检查引用该资源的文档如何更新。与其把文档和资源都设成相同保存期限,不如分别说明它们允许多晚发生变化。
部署期间,新旧文档可能暂时并存。新资源准备好之前就发布新文档,或者过早删除旧资源,都可能引发另一类故障。在这个虚构案例中,可以同时检查新文档引用的文件是否存在,以及旧文档引用的文件是否仍能访问。改文件名是缓存设计的工具,并不能自动解决所有部署顺序和回滚问题。
5. 在 Network 面板中逐个检验假设
亲自检查时,先在允许缓存的条件下保留基准记录。同时查看准确的 URL 和查询参数、页面显示的时间表版本、响应状态、请求中的 If-None-Match,以及响应中的 Cache-Control、ETag。即使名称相同,请求标头和响应标头也来自不同主体的条件,因此要记清读的是哪一侧。Chrome DevTools Network 文档说明了如何在 Headers 中分别查看这些项目。
| 虚构的测试情景 | 一起查看的记录 | 不应贸然得出的结论 |
|---|---|---|
| 再次请求同一时间表 | 条件请求、状态、页面时间 | 因为是 304,所以完全没有通信 |
| 修改原始时间后请求 | 新正文、ETag、实际请求 URL | 出现保存成功提示,就已对全部用户生效 |
| 关闭浏览器缓存后请求 | 首次下载与普通再次访问的差别 | 这个结果等于日常再次访问的速度 |
| 通过后退返回 | 是否产生新请求及恢复的页面 | 页面出现,就一定发生了新的 HTTP 验证 |
接着将禁用浏览器缓存作为另一种条件测试。Chrome 的 Disable cache 设置在 DevTools 开启期间生效,因此要同时记录工具保持开启的状态和设置值。它有助于观察首次下载,却不同于检查普通再次访问如何使用存储副本的测试。若没有记录是否开启设置,就可能根据每次都重新下载的结果,误认为缓存没有工作。保留实验条件,以便恢复允许缓存后进行比较。
观察时,避免同时进行多次修改和请求,更有利于缩小原因范围。先在内容保持不变时检查再次访问,再只改一个时间字段,查看响应如何变化。若同时改文档 URL、服务器设置和文件名,就很难区分哪项变化造成了结果。当预期的 200 或 304 没有出现时,应先重读实际请求与响应,再考虑增加设置。
6. 同时检查其他存储路径与完成标准
禁用浏览器的 HTTP 缓存,不代表服务器前的 CDN 或应用程序其他存储路径也已消失。可以分别查看服务工作线程是否处理请求、中间服务器如何选择响应,以及应用是否把已获取的数据保留在页面上。不能仅因 URL 相同,就认为各层使用的是同一份副本。
后退操作也要与新的 HTTP 请求分开检查。浏览器恢复之前会话的页面时,即使带有 no-cache,也可能看不到预期的重新验证请求。立刻据此认为服务器忽略标头,会导致误判。分别记录新导航、刷新和后退时是否真的有请求,才能避免把不同现象混成一个问题。
时间表示例的完成标准,不只是缓存命中率。还要确认内容未变时是否正确利用副本、变更后是否在允许时间内显示新信息、响应的共享对象是否正确。记录变更与观察时刻、条件和实际正文,便于他人重现相同情景。即使速度指标改善了,只要错误开放时间仍长期显示,就没有满足最初要求。
用于生产环境时,应按响应类型一起检查策略、更新和回退程序。不要把公开文档的缓存指令复制到个人响应,也不要把验证成功理解成权限检查已经完成。标头传达的是存储与复用规则。能说明存储了什么、经过哪些确认才得到当前页面,才更容易准确诊断旧页面问题。