浏览器的缓存

553 阅读9分钟

前言

缓存可以说是一种简单高效的性能优化方式了。一个优秀的缓存策略可以减少延迟,减少带宽。

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

浏览器缓存

从缓存位置的角度来说,缓存分为Service Worker、Memory Cache、Disk 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中获取的内容。

Memory Cache

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

那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢?这是不可能的。计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。

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

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

Disk Cache

Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,相比Memory Cache胜在容量和存储时长。

在所有浏览器缓存中Disk Cache覆盖面基本是最大的。它会根据HTTP Herder中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自Disk Cache。

Push Cache

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

  • 所有的资源都能被推送,并且能够被缓存,但是Edge和Safari浏览器支持相对比较差
  • 可以推送no-cache和no-store的资源
  • 一旦连接被关闭,Push Cache就被释放
  • 多个页面可以使用同一个HTTP/2的连接,也就可以使用同一个Push Cache。这主要还是依赖浏览器的实现而定,出于对性能的考虑,有的浏览器会对相同域名但不同的tab标签使用同一个HTTP连接。
  • Push Cache中的缓存只能被使用一次
  • 浏览器可以拒绝接受已经存在的资源推送
  • 你可以给其他域名推送资源

浏览器缓存策略

浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置HTTP Header来实现的。从而达到加快资源获取速度,提升用户体验,减少网络传输,缓解服务端的压力的目的。

强缓存

不需要发送请求到服务端,直接读取浏览器本地缓存,在Chrome的Network中显示的HTTP状态码是200,并且Size显示from disk cache或from memory cache。是否强缓存由Expires、Cache-Control和Pragma 3个Header属性共同来控制。

Expires

Expires的值是一个HTTP日期,在浏览器发起请求时,会根据系统时间和Expires的值进行比较,如果系统时间超过了Expires的值,缓存失效。由于和系统时间进行比较,所以当系统时间和服务器时间不一致的时候,会有缓存有效期不准的问题。

Cache-Control

Cache-Control是HTTP/1.1中新增的属性,在请求头和响应头中都可以使用,常用的属性值有:

  • max-age:单位是秒,缓存时间计算的方式是距离发起的时间的秒数,超过间隔的秒数缓存失效
  • no-cache:不使用强缓存,需要与服务器验证缓存是否新鲜
  • no-store:禁止使用缓存(包括协商缓存),每次都向服务器请求最新的资源
  • private:专用于个人的缓存,中间代理、CDN等不能缓存此响应
  • public:响应可以被中间代理、CDN等缓存
  • must-revalidate:在缓存过期前可以使用,过期后必须向服务器验证

Pragma

Pragma只有一个属性值,就是no-cache,效果和Cache-Control中的no-cache一致,不使用强缓存,需要与服务器对比缓存是否新鲜。

优先级

Expires < Cache-Control < Pragma,这也意味着当设置Pragma为no-cache时,Catch-Control设置为无效。类似于css中的优先级,优先级高的样式才会生效。

协商缓存

当浏览器的强缓存失效的时候或者请求头中设置了不走强缓存,并且在请求头中设置了If-Modified-Since或者If-None-Match的时候,会将这两个属性值传到服务端去验证是否命中协商缓存,如果命中了协商缓存,会返回304状态,加载浏览器缓存,并且响应头会设置Last-Modified或者ETag属性。

ETag/If-None-Match

ETag/If-None-Match的值是一串hash码,代表的是一个资源的标识符,当服务端的文件变化的时候,它的hash码会随之改变,通过请求头中的If-None-Match和当前文件的hash值进行比较,如果相等则表示命中协商缓存。ETag又有强弱校验之分,如果hash码是以"W/"开头的一串字符串,说明此时协商缓存的校验是弱校验的,只有服务器上的文件差异(根据ETag计算方式来决定)达到能够触发hash值后缀变化的时候,才会真正地请求资源,否则返回304并加载浏览器缓存。

Last-Modified/If-Modified-Since

Last-Modified/If-Modified-Since的值代表的是文件的最后修改时间,第一次请求服务端会把资源的最后修改时间放到Last-Modified响应头中,第二次发起请求的时候,请求头会带上上一次响应头中的Last-Modified的时间,并放到If-Modified-Since请求头属性中,服务端根据文件最后一次修改时间和If-Modified-Since的值进行比较,如果相等,返回304,并加载浏览器缓存。

浏览器缓存的应用

知道如何运用浏览器缓存,才算真正地理解了浏览器缓存。

设置

  • 强缓存的设置
Cache-Control: max-age=31536000
  • 协商缓存的设置
Cache-Control: no-cache

首先需要使用Cache-Control: no-cache 使浏览器每次都请求服务器,然后配合ETag或者Last-Modified来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。

场景一

web前端的资源包括css,js,html,image等,这些资源响应到浏览器都是index.css、index.html、index.js、index.png之类的。此时可以这样设置HTTP缓存,对于长期不变化的资源可以设置成强缓存,而对于短期内变化多的可以设置成协商缓存。

场景二

当web前端的资源经过了gulp/webpack之类的工具处理,响应到浏览器都是index.hash.css之类的时候,你可以将这些资源设置成强缓存。为什么这种情况下要设置强缓存呢?你可以这样理解,index.hash.css设置成强缓存,只有过期时间超出了,该文件才会重新从服务器请求获取一次。可能你会说该文件在过期时间没超出的时候重新修改了,不是有问题吗?其实不会,因为文件修改了,你会重新使用gulp/webpack处理一次,处理后的文件的hash值改变了,也就是文件不同了,浏览器当然没有该文件的缓存,自然会重新从服务器请求一次。

谢谢阅读!