❕Cache-Control
私有缓存、共享缓存(缓存代理)
在计算机领域,最常用的性能优化手段是时空转换,即时间换空间、或空间换时间。HTTP Cache 属于后者,HTTP 传输的每个环节都可以有缓存。
缓存看起来复杂,但基本原理可概括为一句话:没有消息就是好消息,没有请求的请求才是最快的请求。
服务器和客户端使用 Cache-Control
header 字段进行缓存控制,互相协商缓存的使用策略。
相关术语
Cache:会保存 request 和 response,以便后续 request 重用。可以是共享缓存、私有缓存
共享缓存:存在于源服务器(origin server)和客户端(client)之间的缓存,比如 Proxy、CDN。它存储单个 response 并供多个用户重用,因此要避免在共享缓存中存储个性化内容
私有缓存:存在于客户端(client)的缓存,也称为本地缓存、浏览器缓存。它可以为单个用户存储并重复使用个性化内容
存储 Response:当 response 是可缓存的,会将其存储在缓存中。但是,缓存的 response 并不总是按原样重用。通常,缓存就意味着存储响应
重用 Response:为后续的 request 重用缓存的 response
重新验证 response:询问源服务器存储的响应是否 fresh(有效),通常是通过条件请求完成的
Fresh Response:响应是 fresh 的。通常意味着响应可以重复用于后续请求,具体取决于请求指令
Stale Response:响应是陈旧/过期/失效的。通常意味着响应不能按原样重用。缓存存储不需要立即删除过期响应,因为重新验证会让响应从过期变成 fresh。
存储的 HTTP response 有两种状态:fresh 表示响应仍然有效且可以重用,stale 表示响应已经过期
Age:自生成 response 以来经过的时间,用来判断响应是 fresh 还是 stale
当响应存储在共享缓存中时,有必要通知客户端响应的年龄,
Age
字段
1. 浏览器缓存
1.1 标准指令
标准的 Cache-Control
指令(directive、instruction),如下:
no-store
no-cache
max-age
✔
✔
must-revalidate
stale-while-revalidate
stale-if-error
immutable
must-understand
✔
缓存指令的命名规则:
不区分大小写,但是建议用小写(因为某些实现无法识别大写指令)
多个指令之间用
,
分隔有些指令有一个可选参数
若浏览器不能识别它们,就会忽略。如果指令发生冲突,则应遵守最具限制性的指令。
1.2 基本原理
三个条件:
是否允许缓存?
no-store
指令/属性使用缓存前,是否必须验证?
no-cache
指令/属性缓存失效后,是否必须验证?
must-revalidate
指令/属性,通常和max-age
一起使用HTTP 允许在与源服务器断开连接时,重用过期的 response。
must-revalidate
属性就是防止这种情况发生的一种方法:要么回源服务器重新验证,要么生成504
no-store
✔
✔
不允许缓存(不论私有还是共享)
no-cache
✔
✔
可以缓存,但在使用前必须去服务器验证是否有最新版本
max-age
✔
✔
资源的有效时间,单位 s 秒
是基于响应的创建时间 Date
计算的
Expires
绝对时间,已过时,是 HTTP/1.0 里的
更推荐用 max-age
must-revalidate
✔
如果不过期就继续使用,否则就必须去源服务器验证
no-cache
会导致服务器重新验证。可惜,大多数 HTTP/1.0 缓存不支持它,因此历史上用 max-age=0
作为解决方法。又因为只有当缓存与源服务器断开连接时,max-age=0
才会导致过时响应被重用,因此用 must-revalidate
来解决这个问题。所以,所以,以下两种写法等价:
但现在,我们可以简单地使用 no-cache
了。
当点击浏览器的前进、后退按钮时,浏览器只用最基本的请求头,没有 Cache-Control
,所以就会直接利用之前的资源,而不用再进行网络通信了。
1.3 缓存过期
stale-while-revalidate
:缓存可以重用过期的响应,前提是将其重新验证到缓存重新验证将使缓存再次保持 fresh,因此在客户端看来,缓存在该期间始终是 fresh(新鲜的)
如果在此期间没有发生请求,则缓存将变得陈旧,并且下一个请求将正常重新验证。
stale-if-error
:当遇到服务器 error 时,可以额外使用已过期的响应 x 秒。一旦超过了设置的时间,客户端将会收到 error
1.4 避免重新验证
immutable
:当 response 还是 fresh 的时候不用更新,旨在避免不必要的条件请求。
静态资源的现代最佳实践是在其 URL 中包含版本或哈希,在必要时使用新版本号或哈希,以便更新资源(即缓存清除)。当用户 reload 浏览器时,浏览器将向源服务器发送用于验证的条件请求,但此时其实没必要重新验证静态资源,因为它们从未被修改过。immutable
告诉缓存:响应在新鲜时是不可变的,并避免向服务器发出此类不必要的条件请求。
1.5 缓存前提
must-understand
仅在根据 status code 了解缓存要求时,才缓存响应。
2. 缓存代理
缓存代理是增加了缓存功能的代理服务。在没有缓存的时候,代理服务器只有最简单的中转功能,中间不会存储任何数据,每次都是直接转发客户端和服务器的报文。
加入了缓存之后,代理服务收到源服务器发来的响应数据后需要做两件事:把报文转发给客户端、把报文存入自己的 Cache 里。下次再有相同的请求,代理服务器就可以直接发送 304
或者缓存数据,不必再从源服务器那里获取。这样就降低了客户端的等待时间,同时节约了源服务器的网络带宽。
在 HTTP 的缓存体系中,缓存代理的身份十分特殊:
它既是客户端又是服务器:它面向源服务器时是客户端,在面向客户端时又是服务器,所以它既可以用客户端的缓存控制策略,也可以用服务器端的缓存控制策略
既不是客户端又不是服务器:它只是一个数据的中转站,并不是真正的数据消费者和生产者,所以还需要有一些新的
Cache-Control
属性来对它做特别的约束。与客户端缓存相比,代理的缓存可能会为非常多的客户端提供服务
既是客户端 又是服务器
no-store
no-cache
must-revalidate
max-age
stale-while-revalidate
stale-if-error
immutable
must-understand
代理都可以使用
既不是客户端 又不是服务器
private
public
proxy-revalidate
s-maxage
no-transform
约束代理的服务器角色
max-stale
min-fresh
only-if-cached
约束代理的客户端角色
2.1 服务器角色
private
:response 只能存储在私有缓存中,即本地浏览器,不能放在代理上应该给用户的个性化内容添加该指令,尤其是对于登录后收到的 response、用 cookie 管理的 session。否则该响应可能会存储在共享缓存中,最终会被多个用户重复使用,这可能会导致个人信息泄露
public
:response 可以存储在共享缓存中,是完全开放的,谁都可以存,谁都可以用对于带
Authorization
header 的请求对应的 response,一定不能存储在共享缓存中。但是public
,s-maxage
,must-revalidate
指令会解除该限制,也就是将导致此类响应也会存储在共享缓存中如果请求没有
Authorization
header,或者已经在响应中使用了s-maxage
或must-revalidate
,则不需要使用public
proxy-revalidate
:等价于must-revalidate
,只是仅针对代理。只要代理的缓存过期,就必须回源服务器验证,客户端不必回源,只验证到代理这个环节就行了s-maxage
:只限定在代理上能够存多久。对于代理来说优先级高于max-age
no-transform
:禁止代理对缓存的数据进行优化,比如把图片生成 png、webp 等几种格式
2.2 客户端角色
缓存的生存时间
max-stale
:如果代理上的缓存过期了也可以接受,但不能过期太多,超过 x 秒也会不要min-fresh
:缓存必须有效,而且必须在 x 秒后依然有效。
only-if-cached
:只接受代理缓存的数据,不接受源服务器的响应。如果代理上没有缓存或者缓存过期,就应该给客户端返回一个504 Gateway Timeout
2.3 Vary 字段
Vary 字段
Vary
字段是内容协商的结果,相当于报文的一个版本标记。同一个请求,经过内容协商后可能会有不同的字符集、编码、浏览器等版本,比如:
缓存代理必须要存储这些不同的版本。当再收到相同的请求时,代理就读取缓存里的 Vary
,对比请求头里相应的 Accept-Encoding 或 User-Agent 等字段,如果和上一个请求的完全匹配,比如都是 gzip 或 Chrome,就表示版本一致,可以返回缓存的数据。
2.4 缓存清理
缓存代理有时候也会带来负面影响,缓存不良数据,因此需要及时刷新或删除。
缓存清理,Purge,对代理来说也是非常重要的功能。比如:
过期的数据应该及时淘汰,避免占用空间
源站的资源有更新,需要删除旧版本,主动换成最新版(即刷新)
有时会缓存一些本不该存储的信息(比如网络谣言或危险链接),必须尽快把它们删除
清理缓存的方法有很多,比较常用的一种做法是:使用自定义请求方法 PURGE
,发给代理服务器,要求删除 URI 对应的缓存数据。
3. 一些补充
3.1 清除缓存
清除缓存代理:自定义请求方法
PURGE
(如上)清除浏览器端:手动清理、
Clear-Site-Data: cache
(只清除浏览器缓存,不影响中间缓存)
3.2 举点例子
以下例子,并非来自真实配置,纯理论,仅想说明缓存控制的复杂,要考虑的细节还挺多的。
3.3 缓存模式
常见的缓存模式:
默认设置:根据启发式缓存,进行隐式缓存
启发式缓存:HTTP 被设计为尽可能多地缓存,因此即使没有给出
Cache-Control
,如果满足某些条件,response 也会被存储和重用为了避免启发式缓存,最好显式地为所有 response 提供
Cache-Control
header为了确保默认情况下始终传输最新版本的资源,通常会设置成
no-cache
或no-cache, private
缓存清除:常见的最佳实践是,在每次内容发生变化时更改 URL
重新验证:详见条件请求
主要资源:与子资源不同,主资源不能被缓存清除,因为它们的 URL 不能像子资源那样进行修饰
通常会设置
no-cache
(而不是no-store
)添加
Last-Modified
和ETag
header 允许客户端发送条件请求
托管缓存:Service Worker、CDN、缓存代理 等
主要参考
Last updated