0%

HTTP/前端文件缓存

缓存在开发人员的眼里并不陌生,它对于程序的性能和效率的提升起到了至关重要的作用。甚至是在 CPU 与内存这种硬件与硬件间的交互,也存在着多级别的缓存用以提升交互间的性能。同样的,在前端要进行性能的优化,减少客户端与服务端的资源请求,提升客户端页面的加载速度,自然也就摆脱不了缓存这个“魔咒”。而前端文件缓存实质上也就是 HTTP 缓存,而 HTTP 缓存经过 HTTP 版本的迭代升级,也存在着几种不同的缓存设置方式。

缓存类型

对于 HTTP 来说,总共分为如下两种类型的缓存,而强缓存优先级高于协商缓存。也就是说如果客户端已经存在强缓存后,若还未过期,就不会触发协商缓存。而不同版本的 HTTP 协议,设置这两种类型的缓存,所使用的响应、请求头也各不相同。

1.强缓存

强缓存中的“强”一字,指的是让浏览器按照服务端所提供的有效时间来缓存资源,所以看起来就有那么一点强制的感觉。而服务端一般使用如下两种 HTTP 响应头部属性,来达到让客户端把资源以强缓存的形式存入缓存数据库。

1) Expires

1
2
3
4
# 服务端响应头部
...
Expires: Wed, 21 Oct 2019 00:00:00 GMT
...

Expires 是服务端用于强缓存的 HTTP 1.0 响应头属性,其值为服务端返回的资源到期(有效)时间,既下一次请求该资源时,客户端会到缓存数据库中取出所缓存资源的 Expires 属性值并与此次资源请求的时间做比较,若请求时间小于该值,就直接使用缓存数据库中所缓存的资源。

2) Cache-Control

1
2
3
4
5
6
# 服务端响应头部
...
Cache-Control:public, max-age=31536000
# Expires会被忽略
Expires: Wed, 21 Oct 2019 00:00:00 GMT
...

上文已经提到 Expires 是 HTTP 1.0 的响应头属性,所以目前已逐渐被淘汰。这样 HTTP 1.1 中的 Cache-Control 就顺势就替代了它,并且 ExpiresCache-Control 同时存在时,Cache-Control 的优先级要高于 ExpiresCache-Control 相较于 Expires 具备更多的特性,指定资源有效时间也改为了相对时间并以秒为单位,而其值就包括如下选项:

  1. private:仅客户端可以缓存该资源
  2. public:客户端和代理服务器都可以缓存该资源
  3. max-age=xxx:在客户端中缓存的资源将在资源请求发送后的 xxx 秒后失效
  4. s-maxage=xxx:在代理服务器中缓存的资源将在资源请求发送后的 xxx 秒后失效
  5. no-cache:不使用强缓存,需要使用协商缓存
  6. no-store:资源禁止被缓存,需要每次都请求服务端获取资源
  7. immutable:对于同时设置了 max-age 和 immutable 的资源,如果还在有效期内,用户就算点击了刷新也不再请求服务器获取资源(仅在 HTTPS 下有效)

当客户端在下一次请求该资源时,客户端会到缓存数据库中取出所缓存资源的 Cache-Control 属性值的 max-age 和上一次资源请求的时间相加并与此次的资源请求时间做比较,若请求时间小于相加值,就直接使用缓存数据库中所缓存的资源。

3) Cache-Control 相较于 Expires 的优势

上文已经说过 Cache-Control 和 Expires 分别是 HTTP 1.1 和 HTTP 1.0 用于强缓存的 HTTP 响应头属性。那么为什么有了 Expires 后,还要在新版本中增加多一个 Cache-Control? Cache-Control 主要是用来解决以下几个问题:

  1. Expires 的值是服务端生成的一个绝对时间,这样的话若客户端与服务端存在时差,就会出现缓存命中误差的情况。
  2. Expires 缺少更加精确控制强缓存的特性,如 Cache-Control 中的:private、public、no-cache、no-store 等等

2.协商缓存

协商缓存顾名思义,就是让客户端与服务端协商当前所缓存的资源是否还是有效可用。而服务端一般在客户端第一次进行资源请求时使用如下两种 HTTP 响应头部属性,告知客户端下次再请求该资源时,在请求头附上在上一次服务端响应时所接收到的属性值来让服务端进行缓存资源的有效性和可用性的判断。

1) Last-Modified 与 If-Modified-Since

1
2
3
4
# 服务端响应头部
...
Last-Modified: Wed, 21 Oct 2019 00:00:00 GMT
...
1
2
3
4
# 客户端请求头部
...
If-Modified-Since: Wed, 21 Oct 2019 00:00:00 GMT
...

Last-Modified 是服务端用于协商缓存的 HTTP 1.0 响应头属性,而 If-Modified-Since 则是客户端用于协商缓存的 HTTP 1.0 请求头属性,其值一般是资源在服务端的最后修改时间。

  • 在客户端第一次向服务端请求资源时,服务端会在其响应头部加入 Last Modified 属性,告知该资源的最后修改时间
  • 客户端在接收到服务端的响应后会按照响应头中的缓存标识,在缓存数据库中存入资源及其缓存标识
  • 在客户端下一次请求该资源时,会在响应头中加入 Last-Modified 对应的响应头属性 If-Modified-Since 并填上其值。
  • 服务端接收请求后就会取出客户端请求头中的 If-Modified-Since 属性值与该资源的最后修改时间做比较:
    • 若资源的最后修改时间大于 If-Modified-Since 的值,说明资源有更新的版本,返回 200 状态响应,并在响应头中设置新 Last-Modified 属性值和在其请求体中附上更新的资源
    • 若资源的最后修改时间小于 If-Modified-Since 的值,说明资源无更新的版本,返回 304 状态响应,告知客户端资源无更新,可以使用所缓存的资源

2) Etag 和 If-None-Match

1
2
3
4
# 服务端响应头部
...
ETag: "44a64df551425fcc55e4d42a148795d9f25f89d5"
...
1
2
3
4
# 客户端请求头部
...
If-None-Match: "44a64df551425fcc55e4d42a148795d9f25f89d5"
...

跟上文一样,Etag 也是服务端用于协商缓存的响应头属性,而 If-None-Match 同样也是客户端用于协商缓存的请求头属性,不过 EtagIf-None-Match 是在 HTTP 1.1 中才出现的,并且其值是资源在服务端的唯一标识(生成规则由服务端决定)。

  • 在客户端第一次向服务端请求资源时,服务端会在其响应头部加入 Etag 属性,告知该资源的唯一标识
  • 客户端在接收到服务端的响应后会按照响应头中的缓存标识,在缓存数据库中存入资源及其缓存标识
  • 在客户端下一次请求该资源时,会在响应头中加入 Etag 对应的响应头属性 If-None-Match 并填上其值
  • 服务端接收请求后就会取出客户端请求头中的 If-None-Match 属性值与该资源目前的唯一标识做比较:
    • 若不一致,说明资源有更新的版本,返回 200 状态响应,并在响应头中设置新 Etag 属性值和在其请求体中附上更新的资源
    • 若一致,说明资源无更新的版本,返回 304 状态响应,并在响应头中设置 Etag 属性值(与 Last-Modified 不同),告知客户端资源无更新,可以使用所缓存的资源

3) Etag 相较于 Last-Modified 的优势

上文已经说过 Etag/If-None-Match 和 Last-Modified/If-Modified-Since 分别是 HTTP 1.1 和 HTTP 1.0 用于协商缓存的 HTTP 响应和请求的头属性。那么为什么有了 Last-Modified 后,还要在新版本中增加多一个 Etag?Etag 主要是用来解决以下几个问题:

  1. 一些文件的修改时间也许会被周期性的修改,但其内容并无变化,这个时候就不希望客户端重新获取该资源
  2. 某些文件的修改非常频繁,比如在秒级以内的短期修改,而 Last-Modified 检测到的时间最小粒度仅为秒级,这样就无法精确的让客户端获取资源的最新版本
  3. 某些服务端无法精确的得到资源的最后修改时间

这时,利用 Etag 就能更加精确的控制资源缓存,因为 Etag 是服务端程序自动生成的资源唯一标识,资源每次变动 ,都会生成新的 Etag 值。并且 Last-Modified 和 Etag 是可以同时使用,但是 Etag 优先级要高于 Last-Modified。

缓存步骤整体概括

  1. 在客户端第一次 HTTP 请求服务端资源时,服务端会把该资源的强缓存标识和协商缓存标识(如:Expires、Cache-Control、Last-Modified、Etag 等头部属性)设置于 HTTP 响应的头部中
  2. 客户端在接收到服务端的响应后,会把响应头部的相关缓存标识与资源一同存入缓存数据库中
  3. 客户端再次请求该资源时,会先到本地的缓存数据库中查找是否存在该资源的缓存,资源缓存存在时,则会取出当时一同存入的服务端强缓存标识中资源的有效期值(如:Expires 的值、Cache-Control 的 max-age 的值)进行如下判断:
    • 若还处于强缓存的有效期内,则直接使用所缓存的资源
    • 若已经过了强缓存有效期,则判断是否存在协商缓存标识(如:Last-Modified、Etag)
      • 若存在协商缓存标识,则在请求服务端的请求头中设置对应的协商缓存标识(如:If-Modified-Since、If-None-Match)
      • 若不存在协商缓存标识,则直接请求服务端获取资源,后续将轮回到第 1 步

HTTP缓存概括

Cache-Control 的 max-age=0 和 no-cache 的区别

他们两个在浏览器操作上的区别就是,max-age=0 相当于用户在浏览器按下了 F5(CTRL+R) 进行刷新操作,而 no-cache 则是相当于用户在浏览器同时按下了 CTRL+F5 进行了强制刷新操作。那么它们其本质的区别是什么?好奇宝宝请看下面:

  1. max-age=0:当客户端在请求头中设置 Cache-Control 属性且值为 max-age=0 时,那么客户端会从缓存数据库中取出所缓存资源的 Etag(或 Last-Modified),并在请求头中设置 Etag (或 Last-Modified)在 HTTP 请求中所对应的 If-None-Match(或 If-Modified-Since) 属性(没错 Etag 和 Last-Modified 是 HTTP 响应头部的属性),服务端在收到请求后,会取出 If-None-Match(或 If-Modified-Since) 的值并找到对应的资源进行如下判断:

    • 若无更新则返回 304 状态响应,告知客户端资源无更新,可以使用所缓存的资源
    • 若有更新则返回 200 状态响应,并在响应头设置新 Etag(或 Last-Modified) 属性值和在其请求体中附上更新的资源
  2. no-cache:当客户端在请求头中设置 Cache-Control 属性且值为 no-cache 时,那么客户端将无视缓存数据库中所缓存的资源和其 Etag,直接向服务端发送一个全新的资源请求。

最后用一张图说明用户行为对 HTTP 缓存的影响

用户行为对缓存的影响