从一瓶牛奶理解缓存

142 阅读11分钟

为什么要有缓存

浏览器缓存本质上是为了提升网页加载速度。 具体流程是浏览器将用户请求过的静态资源,存储到电脑本地,当浏览器再次访问时,就可以直接从本地加载,不需要再去服务端请求了。
因此它的优势是:

  1. 对于后端:减少了服务器的负担;
  2. 对于网络:减少了多余网络数据传输;
  3. 对于前端:提升性能,加快了客户端网页的响应速度。

缓存的位置

缓存的位置有:Memory Cache,Service Worker,Disk Cache和Push Cache

Memory Cache

Memory Cache是内存中的缓存,主要有三个特点:(读取快);有时效性(进程死,他也死) ;(能用来存储内存的空间小)。

Memory Cache 也就是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。一旦我们关闭Tab页面,内存中的缓存也就被释放了,再次重新打开相同页面时不会出现from memory cache的情况。
那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢?答案是这是不可能的。计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。

缓存1.png

Service Worker

Service Worker是一个可以更自由地实现缓存功能的线程。

Service Worker是运行在浏览器背后的独立线程,运行在主线程之外,由于它脱离了浏览器的窗体,因此无法直接访问到Dom。但仍可以帮我们完成很多功能,一般可以用来实现缓存功能。ServiceWorker检查资源是否存在其缓存中,并根据其编程的缓存策略决定是否返回资源。
使用Service Worker的话,传输协议必须为HTTPS。因为Service Worker 中涉及到请求拦截,所以必须使用HTTPS协议来保障安全。
Service Worker的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。

Disk Cache

Disk Cache 是存储在硬盘中的缓存,相比于memory cache读取速度,但可持续(进程死了,它还活着)且存储容量大

Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之Memory Cache胜在容量和存储时效性上。
在所有浏览器缓存中,Disk Cache覆盖面基本是最大的。它会根据HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。关闭浏览器后,数据依然存在,此资源不会随着该页面的关闭而释放掉下次打开仍然会是from disk cache。

缓存2.png

上面提到了memory cache 会随进程的关闭而释放,所以我们可以看到,同一个资源,当我们首次进入的时候,文件基本都来自disk cache;在刷新之后,部分文件变成了memory cache;当新开一个tab的时候,又会变成 disk cache。

缓存3.gif

Push Cache

Push Cache是HTTP2在 server push阶段存在的缓存,优先级最低。

Push Cache 是HTTP2中的内容,是HTTP2在 server push阶段存在的缓存,当以上三种缓存都没有命中时,它才会被使用,优先级最低。同时,其缓存时间也很短,只针对在统一会话(Session)中存在,一旦会话结束就被释放。其具有以下特点:

  • 所有的资源都能被推送,可以推送no-cache和no-store的资源
  • 时间较短,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令,连接一旦被关闭,就会被释放
  • 多页面可以使用相同的HTTP/2连接,也就是可以使用同一个Push Cache。这主要还是依赖浏览器的实现而定,出于对性能的考虑,有的浏览器会对相同域名但不同的tab标签使用同一个HTTP连接。
  • Push Cache 中的缓存只能被使用一次
  • 浏览器可以拒绝接受已经存在的资源推送
  • 你可以给其他域名推送资源
缓存4.png

浏览器缓存机制

从一瓶牛奶理解缓存的执行流程

后端给了前端一瓶奶,上面写着最佳饮用时间24h(强缓存max-age),还写着牛奶编号7300(协商缓存的Etag)和生产日期(协商缓存的LastModified)。

结合这个我们来说浏览器缓存的执行流程:
浏览器先看该缓存是否在最佳饮用时间内,如果在,直接使用。如果不在,向服务器发送请求查看牛奶编号和生成日期看看这瓶牛奶过期了吗,如果没有过期,也可以使用;如果过期了,则需要重新加载缓存。
再来一起看看这个图更具体地理解下~

缓存5.png

1.浏览器先根据这个资源的http头信息来判断是否命中强缓存。如果命中则直接加载缓中的资源,并不会将请求发送到服务器。
2.如果未命中强缓存,则浏览器会将资源加载请求发送到服务器。服务器来判断浏览器本地缓存是否失效。若可以使用,则服务器并不会返回资源信息,浏览器继续从缓存加载资源,这就是协商缓存。协商缓存会发请求到服务器。
3.如果未命中协商缓存,则服务器会将完整的资源返回给浏览器,浏览器加载新资源,并更新缓存。

强缓存

强缓存未过期可以直接使用不需请求。可通过设置两种 HTTP Header实现: Expires(缓存过期时间)和Cache-Control(更多个性化设置)。

命中强缓存时,浏览器并不会将请求发送给服务器。在Chrome的开发者工具中看到http的返回码是200,但是在Size列会显示为(form memorycache or from disk cache)。强缓存可以通过设置两种 HTTP Header实现: Expires和Cache-Control。

Expires: 是http 1.0的字段,缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。
缺点:受限于本地时间,如果修改了本地时间,可能会造成缓存失效

Cache-Control: 是http 1.1中的属性,它提供了对资源的缓存的更精确的控制。它有很多不同的值:

  • public:设置了该字段值的资源表示可以被任何对象(包括:发送请求的客户端、代理服务器等等)缓存。这个字段值不常用,一般还是使用max-age=来精确控制;
  • private:设置了该字段值的资源只能被用户浏览器缓存,不允许任何代理服务器缓存。在实际开发当中,对于一些含有用户信息的HTML,通常都要设置这个字段值,避免代理服务器(CDN)缓存;
  • no-store:完全不缓存
  • no-cache:不使用强缓存,但可以用协商缓存
  • max-age=:设置缓存的最大有效期,单位为秒
  • s-maxage=:优先级高于max-age=,仅适用于共享缓存(CDN)。
  • max-stale[=]:设置了该字段表明客户端愿意接收已经过期的资源,但是不能超过给定的时间限制。

协商缓存

什么是协商呢?就是浏览器发送请求到服务器询问我请求的文件是否更新过期。

  • 没有更新过期,浏览器可以使用缓存,返回304,不会返回资源内容。
  • 过期了,浏览器不可以使用缓存,服务器发送新的资源到浏览器。状态码200

若未命中强缓存,则浏览器会将请求发送至服务器。服务器根据http头信息中的Last-Modified / If-Modified-Since或Etag / If-None-Match来判断是否命中协商缓存。如果命中,则http返回码为304,浏览器从缓存中加载资源。

Last-Modified/lf-Modified-Since:浏览器在第一次访问资源时,服务器返回资源的同时,在response header中添加Last-Modified的header,值是这个资源在服务器上的最后修改时间,浏览器接收后缓存文件和header;
浏览器下一次请求这个资源,浏览器检测到有Last-Modified这个header,于是添加lf-Modified-Since这个header,值就是Last-Modified中的值;服务器再次收到这个资源请求,会根据If-Modified-Since 中的值与服务器中这个资源的最后修改时间对比,如果没有变化,返回304和空的响应体,直接从缓存读取,如果If-Modified-Since的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和200
缺点:

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

Etag/lf-None-Match: Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,Etag就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到request header里的If-None -Match里,服务器只需要比较客户端传来的If-None-Match跟自己服务器上该资源的Etag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现Etag匹配不上,那么直接以常规GET 200回包形式将新的资源((当然也包括了新的ETag))发给客户端;如果Etag是一致的,则直接返回304知会客户端直接使用本地缓存即可。

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

各种刷新的区别

打开网页直接输入地址

如有缓存则使用,没有则发送网络请求。
用刚刚牛奶的例子来说,如果此时手里有牛奶,不管是否过期,直接喝;没有就去找后端要。

Command+ R(正常重新加载)

还是用牛奶的例子来说,就是正常流程。浏览器先看该缓存是否在最佳饮用时间内,如果在,直接使用。如果不在,向服务器发送请求查看牛奶编号和生成日期看看这瓶牛奶过期了吗,如果没有过期,也可以使用;如果过期了,则需要重新加载缓存。

Command + shift +R(硬性重新加载)

继续用牛奶的例子,用到哪杯牛奶,就直接找后端要杯新的。

缓存6.png

清空缓存并硬性重新加载

就是字面意思,先清空缓存数据,再从服务器请求资源。既然都是重新从服务器请求资源,那跟硬性重新加载的区别是什么呢?页面加载后,还可能通过JS加载内容,只是普通的硬性重新加载的话这部分的内容仍然是从缓存中加载的,清空缓存则可以保证页面内容是完全重新加载的。
同样用牛奶的例子,把手里所有的牛奶都扔掉,再按需找后端要。

彩蛋:阿廖沙的小屋(https://alyosha.top/ )