浏览器缓存

811 阅读12分钟

前言

缓存可以说是性能优化中简单高效的一种优化方式了。一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷。

对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。

那么下面就从缓存的位置、缓存的方式、缓存的策略及实际的应用场景来解析一下浏览器缓存的相关知识点。

思维导图

缓存位置

从缓存位置分为四种,并且有各自的优先级,按优先级从高到低排序分别是:

  • Service Worker
  • Memory Cache
  • Dish Cache
  • Push Cache

Service Worker

Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。 使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。

Service Worker 实现缓存功能一般分为三个步骤:

  • 首先需要先注册 Service Worker。
  • 然后监听到 install事件以后就可以缓存需要的文件。
  • 那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。

当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。

功能:比如离线缓存消息推送网络代理等功能。其中的离线缓存就是 Service Worker Cache。

Memory Cache

指的是内存中的缓存,主要包含的是当前页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,但是缓存持续性很短,会随着进程的释放而释放。一旦我们关闭Tab页面,内存中的缓存也就被释放了。

内存缓存中有一块重要的缓存资源是preloader相关指令(例如<link rel="prefetch">)下载的资源。preloader的相关指令已经是页面优化的常见手段之一,它可以一边解析js/css文件,一边网络请求下一个资源。

需要注意的事情是,内存缓存在缓存资源时并不关心返回资源的HTTP缓存头Cache-Control是什么值,同时资源的匹配也并非仅仅是对URL做匹配,还可能会对Content-Type,CORS等其他特征做校验。

Disk Memory

指的是存储在硬盘中的缓存,从存取效率上讲是比内存缓存慢的,但是他的优势在于存储容量和存储时长。

浏览器会把哪些文件丢进内存中?哪些丢进硬盘中?

对于大文件来说,大概率是不存储在内存中的,反之优先。
当前系统内存使用率高的话,文件优先存储进硬盘。

Push Cache

Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。

如果以上四种缓存都没有命中的话,那么只能发起请求来获取资源了。那么为了性能上的考虑,大部分的接口都应该选择好缓存策略,通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。

缓存方式

缓存方式分为以下两种,按优先级排序为:

  • 强缓存
  • 协商缓存

强缓存

缓存作用分为两种情况,一种是需要发送HTTP请求,另一种是不需要发送,而检查强缓存是不需要发送HTTP请求的,直接从缓存中读取资源,在Chrome控制台的Network选项中可以看到该请求返回200的状态码,并且Size显示from disk cache或者是from memory cache。

强缓存可以通过设置两种HTTP Header实现。

  • Expires(HTTP/1.0)
  • Cache-Control(HTTP/1.1)

Expires

Expires即为缓存过期时间,指定资源到期的事件,存在于服务器返回的响应头中,告诉浏览器在这个过期时间之内可以直接从缓存中获取数据,而不需要再次发送请求。
字段长这样: Expires: Sat, 4 July 2020 13:26:00 GMT
表示资源在2020年7月4号13点26分过期,过期了就要向服务器发送请求。

Expires是HTTP/1.0的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。(即服务器的时间和浏览器的时间可能并不一致)

Cache-Control

它和Exipres字段本质的不同是它并没有采用具体的过期时间点,而是采用过期时长来控制缓存,对应的选项有max-age。
比如:Cache-Control: max-age=300
表示这个请求的响应在300s之内再次加载资源,就会命中强缓存。

Cache-Control可以在请求头或者响应头中设置,并且可以组合使用多种指令:

  • public:所有内容都将被缓存(客户端和代理服务器都可以缓存)。例子:
    Browser <-- proxy1 <-- proxy2 <-- Server,中间的代理服务器proxy1可以缓存资源,如果下次请求同一资源,proxy1直接把自己缓存的东西给Browser而不再向proxy2发送请求。
  • private:所有内容只有客户端可以缓存,Cache-Control的默认值。即中间节点不允许缓存对于Browser <-- proxy1 <-- proxy2 <-- Server,proxy会把Server返回的数据发送给proxy1,自己不缓存任何数据。当下次Browser再次请求时proxy会做好请求转发而不是给自己缓存的数据。
  • no-store:所有内容都不会被缓存,既不使用强缓存也不使用协商缓存。
  • no-cache:客户端缓存内容,是否使用缓存则需要经过协商缓存来验证决定。即跳过当前的强缓存,发送HTTP请求,即进入协商缓存阶段。
  • max-age:max-age=xxx表示缓存内容将在xxx秒后失效。
  • s-maxage:作用同max-age,只在代理服务器中生效。 s-maxage的优先级高于max-age。
  • max-stale:能容忍的最大过期时间。max-stale指令表明了客户端愿意接收一个已经过期了的响应。如果指定了max-stale的值,则最大容忍时间为对应的秒数。如果没有指定,那么说明浏览器愿意接收任何age的响应(age表示响应由源站生成或确认的时间与当前时间的差值)。
  • min-fresh:能够容忍的最小新鲜度。min-fresh标示了客户端不愿意接受新鲜度不多于当前的age加上min-fresh设定的时间之和的响应。

我们可以将多个指令配合起来一起使用,达到多个目的。比如说我们希望资源能被缓存下来,并且是客户端和代理服务器都能缓存,还能设置缓存失效时间等等。

Expires和Cache-Control两者对比

区别就在于 Expires是HTTP/1.0的产物,Cache-Control是HTTP/1.1的产物,两者同时存在的话,Cache-Control优先级高于Expires;在某些不支持HTTP1.1的环境下,Expires就会发挥用处。所以Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法。 强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容是否已经发生了更新呢?此时我们需要用到协商缓存策略。

协商缓存

协商缓存就是强制缓存失效后(强缓存时间到期),浏览器携带缓存标识(tag)向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,tag主要有以下两种情况:

  • Last-Modified
  • ETag

Last-Modified

Last-Modified为最后修改时间。在浏览器第一次给服务器发送请求的时候,服务器会在响应头中加上这个字段。

浏览器接收后,如果再次发送请求,会在请求头中携带If-Modified-Since字段,这个字段的值就是服务器传来的资源最后修改时间

服务器拿到请求头的If-Modified-Since字段,会和服务器中该资源的最后修改事件做对比

  • 如果请求头的值小于最后修改时间,说明资源更新,返回新的资源,跟常规的HTTP请求响应一样。
  • 否则返回304状态码和Not Modified,告诉浏览器直接使用缓存。

使用Last-Modified的弊端:

  • 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
  • 因为Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回已被修改过的资源

根据文件修改时间来决定是否缓存尚有不足,能否可以直接根据文件内容是否修改来决定缓存策略?所以在 HTTP/1.1 出现了 ETag 和If-None-Match。

Etag

Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,Etag就会重新生成。

浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到请求头的If-None-Match字段里,服务器需要比较客户端传来的If-None-Match跟自己服务器上该资源的Etag是否一致,就可以判断资源相对于客户端而言是否已经被修改过了。有以下两种情况:

  • 如果ETag是一致的,则直接返回304,通知客户端直接使用本地缓存即可。
  • 如果服务器发现ETag匹配不上,那么直接以常规请求形式将新的资源(包括了新的ETag)发给客户端。

两者对比

  • 在精确度上,Etag要优于Last-Modified。 Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度**;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。
  • 在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
  • 在优先级上,服务器校验优先考虑Etag

缓存策略

强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回304,继续使用缓存。

缓存机制

用户行为对浏览器缓存的影响

所谓用户行为对浏览器缓存的影响,指的就是用户在浏览器如何操作时,会触发怎样的缓存策略。主要有 3 种:

  • 打开网页,地址栏输入地址: 查找 disk cache中是否有匹配。如有则使用;如没有则发送网络请求。
  • 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache。
  • 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache(为了兼容,还带了 Pragma: no-cache),服务器直接返回 200 和最新内容。

本文参考了 深入理解浏览器的缓存机制