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),如下:

Cache-Control 指令
Request
Response

no-store

no-cache

max-age

must-revalidate

stale-while-revalidate

stale-if-error

immutable

must-understand

缓存指令的命名规则:

  • 不区分大小写,但是建议用小写(因为某些实现无法识别大写指令)

  • 多个指令之间用 , 分隔

  • 有些指令有一个可选参数

若浏览器不能识别它们,就会忽略。如果指令发生冲突,则应遵守最具限制性的指令。

1.2 基本原理

Cache-Control: no-store, no-cache, must-revalidate, max-age=30

三个条件:

  1. 是否允许缓存?no-store 指令/属性

  2. 使用缓存前,是否必须验证?no-cache 指令/属性

  3. 缓存失效后,是否必须验证?must-revalidate 指令/属性,通常和 max-age 一起使用

    • HTTP 允许在与源服务器断开连接时,重用过期的 response。must-revalidate 属性就是防止这种情况发生的一种方法:要么回源服务器重新验证,要么生成 504

指令
Request
Response
说明

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 来解决这个问题。所以,所以,以下两种写法等价:

Cache-Control: no-cache
Cache-Control: max-age=0, must-revalidate

# 但是现在,可以简单地使用
Cache-Control: no-cache

但现在,我们可以简单地使用 no-cache 了。

# 用户强制 reloading 页面时,浏览器会添加
Cache-Control: no-cache

# 当用户 reloading 页面时,浏览器会添加,表示客户端要最新数据
Cache-Control: max-age=0

当点击浏览器的前进、后退按钮时,浏览器只用最基本的请求头,没有 Cache-Control,所以就会直接利用之前的资源,而不用再进行网络通信了。

1.3 缓存过期

  1. stale-while-revalidate:缓存可以重用过期的响应,前提是将其重新验证到缓存

    • 重新验证将使缓存再次保持 fresh,因此在客户端看来,缓存在该期间始终是 fresh(新鲜的)

    • 如果在此期间没有发生请求,则缓存将变得陈旧,并且下一个请求将正常重新验证。

  2. stale-if-error:当遇到服务器 error 时,可以额外使用已过期的响应 x 秒。一旦超过了设置的时间,客户端将会收到 error

# 有效期是7天,如果失效了必须验证
Cache-Control: max-age=604800, must-revalidate

# 有效期是7天,如果失效了还可以额外再用1天,但前提是必须验证
Cache-Control: max-age=604800, stale-while-revalidate=86400

# 有效期是7天,当遇到错误了还可以额外再用1天,超过1天的就会正常报错了
Cache-Control: max-age=604800, stale-if-error=86400

1.4 避免重新验证

immutable:当 response 还是 fresh 的时候不用更新,旨在避免不必要的条件请求。

静态资源的现代最佳实践是在其 URL 中包含版本或哈希,在必要时使用新版本号或哈希,以便更新资源(即缓存清除)。当用户 reload 浏览器时,浏览器将向源服务器发送用于验证的条件请求,但此时其实没必要重新验证静态资源,因为它们从未被修改过。immutable 告诉缓存:响应在新鲜时是不可变的,并避免向服务器发出此类不必要的条件请求。

1.5 缓存前提

must-understand 仅在根据 status code 了解缓存要求时,才缓存响应。

2. 缓存代理

缓存代理是增加了缓存功能的代理服务。在没有缓存的时候,代理服务器只有最简单的中转功能,中间不会存储任何数据,每次都是直接转发客户端和服务器的报文。

加入了缓存之后,代理服务收到源服务器发来的响应数据后需要做两件事:把报文转发给客户端、把报文存入自己的 Cache 里。下次再有相同的请求,代理服务器就可以直接发送 304 或者缓存数据,不必再从源服务器那里获取。这样就降低了客户端的等待时间,同时节约了源服务器的网络带宽。

在 HTTP 的缓存体系中,缓存代理的身份十分特殊:

  1. 它既是客户端又是服务器:它面向源服务器时是客户端,在面向客户端时又是服务器,所以它既可以用客户端的缓存控制策略,也可以用服务器端的缓存控制策略

  2. 既不是客户端又不是服务器:它只是一个数据的中转站,并不是真正的数据消费者和生产者,所以还需要有一些新的 Cache-Control 属性来对它做特别的约束。与客户端缓存相比,代理的缓存可能会为非常多的客户端提供服务

缓存代理
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 服务器角色

  1. private:response 只能存储在私有缓存中,即本地浏览器,不能放在代理上

    • 应该给用户的个性化内容添加该指令,尤其是对于登录后收到的 response、用 cookie 管理的 session。否则该响应可能会存储在共享缓存中,最终会被多个用户重复使用,这可能会导致个人信息泄露

  2. public:response 可以存储在共享缓存中,是完全开放的,谁都可以存,谁都可以用

    • 对于带 Authorization header 的请求对应的 response,一定不能存储在共享缓存中。但是 public, s-maxage, must-revalidate 指令会解除该限制,也就是将导致此类响应也会存储在共享缓存中

    • 如果请求没有 Authorization header,或者已经在响应中使用了 s-maxagemust-revalidate,则不需要使用 public

  3. proxy-revalidate:等价于 must-revalidate,只是仅针对代理。只要代理的缓存过期,就必须回源服务器验证,客户端不必回源,只验证到代理这个环节就行了

  4. s-maxage:只限定在代理上能够存多久。对于代理来说优先级高于 max-age

  5. no-transform:禁止代理对缓存的数据进行优化,比如把图片生成 png、webp 等几种格式

# 代理不能缓存,必须直接给客户端,有效时间是5秒,过期了得去源服务器重新请求
Cache-Control: private, max-age=5

# 代理可以缓存,客户端能缓存5秒,代理可以缓存10秒(代理只要没过期就不必回源)
Cache-Control: public, max-age=5, s-maxage=10

# 默认是 public 的
# 代理和客户端都可以存30秒,过期后代理必须重新请求源服务器,且代理不能擅自做优化或修改
Cache-Control: max-age=30, proxy-revalidate, no-transform

# 在源服务器设置完 Cache-Control Header 之后,
#    必须加上 Last-modified 或 ETag Header 字段
#    否则,客户端和代理后面就无法使用条件请求来验证缓存是否有效,也就不会有 304 缓存重定向

2.2 客户端角色

  1. 缓存的生存时间

    • max-stale:如果代理上的缓存过期了也可以接受,但不能过期太多,超过 x 秒也会不要

    • min-fresh:缓存必须有效,而且必须在 x 秒后依然有效。

  2. only-if-cached:只接受代理缓存的数据,不接受源服务器的响应。如果代理上没有缓存或者缓存过期,就应该给客户端返回一个 504 Gateway Timeout

# response
Cache-Control: max-age=5

# request
Cache-Control: max-stale=2  # 现在已经在代理那存了7秒,虽然过期2秒,但还能用
Cache-Control: min-fresh=1  # 绝对不允许过期的

2.3 Vary 字段

Vary 字段是内容协商的结果,相当于报文的一个版本标记。同一个请求,经过内容协商后可能会有不同的字符集、编码、浏览器等版本,比如:

Vary: Accept-Encoding
Vary: User-Agent        # User-Agent 的值非常丰富多样,会大大降低缓存被重用的机会

缓存代理必须要存储这些不同的版本。当再收到相同的请求时,代理就读取缓存里的 Vary,对比请求头里相应的 Accept-Encoding 或 User-Agent 等字段,如果和上一个请求的完全匹配,比如都是 gzip 或 Chrome,就表示版本一致,可以返回缓存的数据。

2.4 缓存清理

缓存代理有时候也会带来负面影响,缓存不良数据,因此需要及时刷新或删除。

缓存清理,Purge,对代理来说也是非常重要的功能。比如:

  • 过期的数据应该及时淘汰,避免占用空间

  • 源站的资源有更新,需要删除旧版本,主动换成最新版(即刷新)

  • 有时会缓存一些本不该存储的信息(比如网络谣言或危险链接),必须尽快把它们删除

清理缓存的方法有很多,比较常用的一种做法是:使用自定义请求方法 PURGE,发给代理服务器,要求删除 URI 对应的缓存数据。

3. 一些补充

3.1 清除缓存

  • 清除缓存代理:自定义请求方法 PURGE(如上)

  • 清除浏览器端:手动清理、Clear-Site-Data: cache(只清除浏览器缓存,不影响中间缓存)

3.2 举点例子

以下例子,并非来自真实配置,纯理论,仅想说明缓存控制的复杂,要考虑的细节还挺多的。

# 对于具有个性化内容的 response,建议这两个指令同时给出:
Cache-Control: no-store, private

# 但又因为 no-store 不能阻止重用旧缓存,建议再加上 no-cache
Cache-Control: no-store, no-cache, private

# no-store 会阻止存储响应,但不会删除同一 URL 的旧响应
# 也就是说,如果已经为特定 URL 存储了旧响应,则即便返回 no-store 也不会阻止旧响应被重用
# 则可以用 no-cache,强制客户端在重用任何存储的响应之前,先发送验证请求
Cache-Control: no-cache

# 处理过期缓存
Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate

# 如果不想要缓存,不建议直接用 no-store
# 因为这会失去 HTTP 和浏览器所具有的许多优势,包括浏览器的后退/前进缓存
# 不想要缓存,同时还获得 Web 平台完整功能集的优势,最好用 no-cache
Cache-Control: no-cache, private

3.3 缓存模式

常见的缓存模式:

  1. 默认设置:根据启发式缓存,进行隐式缓存

    • 启发式缓存:HTTP 被设计为尽可能多地缓存,因此即使没有给出 Cache-Control,如果满足某些条件,response 也会被存储和重用

    • 为了避免启发式缓存,最好显式地为所有 response 提供 Cache-Control header

    • 为了确保默认情况下始终传输最新版本的资源,通常会设置成 no-cacheno-cache, private

  2. 缓存清除:常见的最佳实践是,在每次内容发生变化时更改 URL

  3. 重新验证:详见条件请求

  4. 主要资源:与子资源不同,主资源不能被缓存清除,因为它们的 URL 不能像子资源那样进行修饰

    • 通常会设置 no-cache(而不是 no-store

    • 添加 Last-ModifiedETag header 允许客户端发送条件请求

  5. 托管缓存:Service Worker、CDN、缓存代理 等

主要参考

Last updated