前端性能优化:HTTP缓存知多少

性能优化

Posted by Mopecat on December 30, 2019

HTTP缓存

按失效策略分类:

  • 强缓存 强缓存是指客户端在第一次请求后,有效时间内不会再去请求服务器,而是直接使用缓存数据。

    • Expires 是一个未来时间的时间戳,表示缓存资源的过期时间,由服务端下发。由于有以下几点缺陷已经被淘汰:

      • 可能会因为服务器和客户端的 GMT 时间不同,出现偏差
      • 如果修改了本地时间,那么客户端端日期可能不准确
      • 写法太复杂,字符串多个空格,少个字母,都会导致非法属性从而设置失效 Expires:Tue, 13 May 2020 09:33:34 GMT
    • Cache-control 现在缓存大多数都是用的这个响应头部,功能非常强大。它具有多个不同的值(下面是比较常用的几个):

      • private: 表示私有缓存,不能被共有缓存代理服务器缓存,不能在用户间共享,可被用户的浏览器缓存。
      • public: 表示共有缓存,可被代理服务器缓存,比如 CDN,允许多用户间共享
      • max-age: 值以秒为单位,表示缓存的内容会在该值后过期(用于替代ExpiresHTTP 规定,如果 Cache-control 的 max-age 和 Expires 同时出现,那么 max-age 的优先级更高,他会默认覆盖掉 expires。
      • no-cache: 需要使用协商缓存,协商缓存的内容我们后面介绍。注意这个字段并不表示不使用缓存
      • no-store: 所有内容都不会被缓存
      • must-revalidate: 告诉浏览器,你这必须再次验证检查信息是否过期, 返回的状态码就不是 200 而是 304

      关于 Cache-control 取值总结,我们可以参考 Google developer 的一个图示

      缓存策略流程图

  • 协商缓存

    强缓存判断的实质上是缓存资源是否超出某个时间或者某个时间段。很多情况是超出了这个时间或时间段,但是资源并没有更新。从优化的角度来说,我们真正应该关心的是服务器端文件是否已经发生了变化。此时我们需要用到协商缓存策略。

    强缓存关于是否使用缓存的决断完全是由浏览器做出的,但是浏览器是不可能知道服务器端的文件是否发生了更改的,所以协商缓存就是将是否启用缓存的决定权交给服务器端,因此协商缓存还是需要一次网络请求的。

    协商缓存过程:在浏览器端,当对某个资源的请求没有命中强缓存时,浏览器就会发一个请求到服务器,验证协商缓存是否命中,如果协商缓存命中,请求响应返回的 HTTP 状态为 304。

    根据 HTTP 协议,这个决断是根据【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】这两对 header 来作出的。

    • Last-Modified,If-Modified-Since 主导的协商缓存过程:

      1. 浏览器第一次请求资源,服务端在返回资源的响应头中加入 Last-Modified 字段,这个字段表示这个资源在服务器上的最近修改时间 Last-Modified: Tue, 12 Jan 2019 09:08:53 GMT
      2. 浏览器收到响应,并记录 Last-Modified 这个响应头的值为 T
      3. 当浏览器再次向服务端请求该资源时,请求头加上 If-Modified-Since 的 header,这个 If-Modified-Since 的值正是上一次请求该资源时,后端返回的 Last-Modified 响应头值 T
      4. 服务端再次收到请求,根据请求头 If-Modified-Since 的值 T,判断相关资源是否在 T 时间后有变化;如果没有变化则返回 304 Not Modified,且并不返回资源内容,浏览器使用资源缓存值;如果有变化,则正常返回资源内容,且更新 Last-Modified 响应头内容

    这种基于时间的判断方式跟Expires的问题有些相似,1. 如果客户端时间不准确,判断就不可靠。 2. Last-Modified只能精确到秒级,如果某些文件 1 秒内呗修改多次,就不能准确地标注修改时间 3.一些文件会周期性修改,但是内容并不改变,仅仅改变的是修改时间,这时候用 Last-Modified就不是很合适了。为了弥补这些缺陷就有了,【ETag、If-None-Match】这一对 header 头来进行协商缓存的判断。

    • ETag、If-None-Match 主导的协商缓存过程:

      1. 浏览器第一次请求资源,服务端在返回资源的响应头中加入 EtagEtag 能够弥补 Last-Modified 的问题,因为 Etag 的生成过程类似文件 hash 值,Etag 是一个字符串,不同文件内容对应不同的 Etag
      1
      2
      
      //response Headers
      ETag: "751F63A30AB5F98F855D1D90D217B356"
      
      1. 浏览器收到响应,记录 Etag 这个响应头的值为 E
      2. 浏览器再次跟服务器请求这个资源时,在请求头上加上 If-None-Match,值为 Etag 这个响应头的值 E
      3. 服务端再次收到请求,根据请求头 If-None-Match 的值 E,根据资源生成一个新的 ETag,对比 E 和新的 Etag:如果两值相同,则说明资源没有变化,返回 304 Not Modified,同时携带着新的 ETag 响应头;如果两值不同,就正常返回资源内容,这时也更新 ETag 响应头
      4. 浏览器收到 304 的响应后,就会从缓存中加载资源

      Etag 优先级比 Last-Modified 高,如果他们组合出现在请求头当中,我们会优先采用 Etag 策略

      同时 Etag 也有自己的问题:相同的资源,在两台服务器产生的 Etag 是不是相同的,所以对于使用服务器集群来处理请求的网站来说, Etag 的匹配概率会大幅降低。(负载均衡的情况)

由上述内容我们看出:为了使缓存策略更加可靠,灵活,HTTP 1.0 版本 和 HTTP 1.1 版本的缓存策略一直是在渐进增强的。这也意味着 HTTP 1.0 版本 和 HTTP 1.1 版本关于缓存的特性可以同时使用,强制缓存和协商缓存也会同时使用。当然他们在混合使用时有优先级的限制,我们通过下面这个流程图来做一个总结:

缓存全流程

按缓存位置分类:

  • memory cache // 内存
  • disk cache // 磁盘
  • Service Worker