Skip to content

前端缓存

前端缓存主要分为浏览器缓存和 HTTP 缓存。

浏览器缓存(基于内存的缓存)

浏览器缓存,或者说是内存缓存,是指缓存在内存中的资源,它仅和当前的标签页相关,当用户关闭当前标签页时,对应的内存缓存将会失效。

浏览器缓存是不受开发者控制的。只要 HTTP 头没有设置 Cache-Control: no-store 的资源,都会经过浏览器缓存阶段,不同的浏览器则会有不同的策略来判断一个文件是否需要存放在内存缓存中。

虽然浏览器的缓存策略不仅相同,但是都基本符合以下几点:

  • 文件体积较小。
  • 如果开启了隐私模式,那么缓存会存放在内存中。
  • 被预加载的资源。 比如设置了 CSS 中的 @import 获取 video 元素的 post 属性,或者 <link rel="preload">

chrome 和 safari 会在网络监控面板标注资源是来自内存缓存还是磁盘缓存,而 firefox 则只会提示已缓存,不会提示来自哪里。

HTTP 缓存的类型

HTTP Caching 标准中,将缓存的类型分为私有缓存和共享缓存。

私有缓存

私有缓存指只有绑定特定的客户端的缓存,通常指浏览器。由于存储的响应不与其他客户端共享,因此私有缓存可以存储该用户的个性化响应。

如果响应包含个性化内容并且你只想将响应存储在私有缓存中,则必须指定 private 指令。

HTTP
Cache-Control: private

请注意,如果响应具有 Authorization 标头,则不能将其存储在私有缓存(或共享缓存,除非 Cache-Control 指定的是 public)中。

共享缓存

在客户端和服务器之间,能够存储用户之间共享的响应。共享缓存可以更加细分为代理缓存和托管缓存。

代理缓存

代理缓存指在客户端和服务端之前的代理服务器存储的缓存文件。 但是由于 HTTPS 的广泛使用,客户端和服务端之间的通信内容对于代理服务器来说是未知的,因此多数的代理服务器只能对内容进行转发,而不能进行缓存。现在已经很少考虑代理服务器的缓存了。

托管缓存

托管缓存由服务开发人员明确部署,包括反向代理、CDN 和 service worker 与缓存 API 的组合。

HTTP 缓存的策略和相关字段

HTTP缓存可以分为强制缓存和协商缓存策略。匹配缓存的大致步骤可以描述为:

  1. 先在强缓存(磁盘缓存)中根据 url 和相关的 http 头字段查找对应的缓存。如果命中缓存,并且缓存在有效期内,那么就直接使用缓存,并返回 200 状态码。
  2. 如果没有命中强缓存,则会走协商缓存阶段。这个时候客户端会尝试询问服务器缓存时候可用。如果缓存可用,则会继续使用本地缓存,并返回 304 状态码。
  3. 如果协商缓存阶段已经知道本地缓存过期了,那么就会重新发送请求,从服务器获取完整的资源,并根据相关的缓存字段,缓存服务器的响应。

基于时间的缓存(强缓存)

HTTP/1.0 中主要使用 Expires 和 Pragma 这两个响应头字段来判断是否缓存资源。

  • Pragma: no-cache 要求客户端不缓存响应数据。
  • Expires: Tue, 28 Feb 2022 22:22:22 GMT 要求客户端缓存响应数据到某个时刻。
响应标头说明
Pragmano-cache与 Cache-Control: no-cache 效果一致。强制要求缓存服务器在返回缓存的版本之前将请求提交到源头服务器进行验证。
Expires<http-date>响应头包含日期/时间,即在此时候之后,响应过期。

由于解析时间格式较为复杂,并且可以通过偏移系统时间产生错误。在 HTTP/1.1 中新增了 Cache-Control 字段来控制缓存。

响应标头说明
Cache-Controlno-cache在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证 (协商缓存验证)。
Cache-Controlno-store缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存。
Cache-Controlmax-age=<seconds>设置缓存存储的最大周期,超过这个时间缓存被认为过期 (单位秒)。与Expires相反,时间是相对于请求的时间。

基于验证的缓存(协商缓存)

过时的响应不会立即被丢弃。HTTP 有一种机制,可以通过询问源服务器将陈旧的响应转换为新的响应。这称为验证,有时也称为重新验证。

验证是通过使用包含 If-Modified-SinceIf-None-Match 请求标头的条件请求完成的。

Last-Modified/If-Modified-Since

如果服务器在上一次的响应时携带了 Last-Modified 时间,那么当强缓存过期时,客户端将发送带有 If-Modified-Since 请求标头的请求,以询问该资源是否已经变更。

如果内容自指定时间以来没有更改,服务器将响应 304 Not Modified,并且将不会返回响应体,客户端需要根据返回的响应字段设置该资源的缓存。

标头说明
响应标头Last-Modified<http-date>表示该资源最后的修改时间。
请求标头If-Modified-Since<http-date>服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 。如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的 304 响应。

ETag/If-None-Match

由于时间格式难以解析,已经分布式系统不好判断文件的最后修改时间,为了解决这些问题,ETag 响应标头被标准化作为替代方案。

ETag 响应标头的值是服务器生成的任意值。服务器对于生成值没有任何限制,因此服务器可以根据他们选择的任何方式自由设置值——例如主体内容的哈希或版本号。

服务器在向客户端发送响应时,会将响应体的签名放在 ETag 响应头中,并发送给客户端。

当客户端的强缓存失效时,客户端会将保存的 ETag 放在 If-None-Match 请求头字段中。

如果服务器为请求的资源确定的 ETag 标头的值与请求中的 If-None-Match 值相同,则服务器将返回 304 Not Modified。

如果服务器确定请求的资源现在应该具有不同的 ETag 值,则服务器将其改为 200 OK 和资源的最新版本进行响应。

标头说明
响应标头ETag<etag_value>HTTP 响应头是资源的特定版本的标识符。
请求标头If-None-Match<If-None-Match>当且仅当服务器上没有任何资源的 ETag 属性值与这个首部中列出的相匹配的时候,服务器端才会返回所请求的资源,响应码为 200 。

在缓存重新验证期间,如果 ETag 和 Last-Modified 都存在,则 ETag 优先。

启发式缓存

即使没有给出 Cache-Control 字段,如果满足条件,客户端也会缓存响应,这被称为启发式缓存。

例如,如果响应返回了以下内容:

HTTP
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2023 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:22:22 GMT

<!doctype html>
...

可以看到,改资源整整一年没有更新。因此客户端会缓存该资源,并保存一段时间,保存的时长取决于不同的浏览器,但是规范建议存储后大约 10%,(本例中为 0.1 年)的时间。

参考资料