前端缓存机制总结

246 阅读10分钟

缓存简介

  1. web缓存是指一个web资源(例如:html页面,js文件,图片,音视频等)存在于web服务器和客户端(浏览器)之间副本。
  2. 缓存可以说是性能优化的一种方式,当我们发起向服务端发起请求时,良好的缓存策略可以帮助我们减少像服务端的请求发送,降低服务器压力,减少带宽,重复利用本地的缓存文件,提高用户的体验。

按照缓存位置分类

  1. Service Worker

  2. Memory Cache

  3. Disk Cache

    优先级由上到下依次查询,如果所有缓存位置都查不到的话,则重新发起网络请求;因为我们前端主要和浏览器端联系紧密,所以我们主要研究浏览器端的缓存级制

Service Worker

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

上述的缓存策略以及缓存/读取/失效的动作都是由浏览器内部判断 & 进行的,我们只能设置响应头的某些字段来告诉浏览器,而不能自己操作。举个生活中去银行存/取钱的例子来说,你只能告诉银行职员,我要存/取多少钱,然后把由他们会经过一系列的记录和手续之后,把钱放到金库中去,或者从金库中取出钱来交给你。

但 Service Worker 的出现,给予了我们另外一种更加灵活,更加直接的操作方式。依然以存/取钱为例,我们现在可以绕开银行职员,自己走到金库前(当然是有别于上述金库的一个单独的小金库),自己把钱放进去或者取出来。因此我们可以选择放哪些钱(缓存哪些文件),什么情况把钱取出来(路由匹配规则),取哪些钱出来(缓存匹配并返回)。当然现实中银行没有给我们开放这样的服务

Service Worker 能够操作的缓存是有别于浏览器内部的 memory cache 或者 disk cache 的。我们可以从 Chrome 的 F12 中,Application -> Cache Storage 找到这个单独的“小金库”。除了位置不同之外,这个缓存是永久性的,即关闭 TAB 或者浏览器,下次打开依然还在(而 memory cache 不是)。有两种情况会导致这个缓存中的资源被清除:手动调用 API cache.delete(resource) 或者容量超过限制,被浏览器全部清空。

如果 Service Worker 没能命中缓存,一般情况会使用 fetch() 方法继续获取资源。这时候,浏览器就去 memory cache 或者 disk cache 进行下一次找缓存的工作了。注意:经过 Service Worker 的 fetch() 方法获取的资源,即便它并没有命中 Service Worker 缓存,甚至实际走了网络请求,也会标注为 from ServiceWorker

Memory Cache

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

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

        当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存。

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

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

Disk Cache

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

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

**        浏览器会把哪些文件丢进内存中?哪些丢进硬盘中?**关于这点,网上说法不一,不过以下观点比较靠得住:

  • 对于大文件来说,大概率是不存储在内存中的,反之优先;

  • 当前系统内存使用率高的话,文件优先存储进硬盘。

按照缓存策略分类

  • 强制缓存(强缓存)
    强缓存是指当浏览器发起请求时,会查找是否有缓存文件,是的话直接使用缓存文件,而不用向服务端去请求资源;这种方式大大节省了带宽的消耗,减轻了服务器的压力,是最能提高性能表现的一种缓存策略;

    我们通过设置请求头的headder的Expires和Cache-control来实现强缓存

  • Expires
    表示缓存到期的时间;是一个绝对的时间(当前时间+缓存时间)
    在响应消息头中,设置这个字段,就可以告诉浏览器,在未过期之前不要发起请求

    Expires: Thu, 10 Oct 2020 08:45:11 GMT
    
    缺点:
  1. 由于是绝对时间,用户可能会将客户端本地的时间进行修改,而导致浏览器判断缓存失效,重新请求该资源。此外,即使不考虑自信修改,时差或者误差等因素也可能造成客户端与服务端的时间不一致,致使缓存失效。
  2. 写法太复杂了。表示时间的字符串多个空格,少个字母,都会导致非法属性从而设置失效
  • Cache-Control

    已知Expires的缺点之后,在HTTP/1.1中,增加了一个字段Cache-control,该字段表示资源缓存的最大有效时间,在该时间内,客户端不需要向服务器发送请求

    这两者的区别就是前者是绝对时间,而后者是相对时间。如下:

    Cache-control: max-age=2592000

下面列举一些 Cache-control 字段常用的值:(完整的列表可以查看 MDN)

  • max-age:即最大有效时间,在上面的例子中我们可以看到
  • must-revalidate:如果超过了 max-age 的时间,浏览器必须向服务器发送请求,验证资源是否还有效。
  • no-cache:虽然字面意思是“不要缓存”,但实际上还是要求客户端缓存内容的,只是是否使用这个内容由后续的对比来决定。
  • no-store: 真正意义上的“不要缓存”。所有内容都不走缓存,包括强制和对比。
  • public:所有的内容都可以被缓存 (包括客户端和代理服务器, 如 CDN)
  • private:所有的内容只有客户端才可以缓存,代理服务器不能缓存。默认值。

Cache-Control的优先级要高于Expires;为了兼容 HTTP/1.0 和 HTTP/1.1,实际项目中两个字段我们都会设置。

  • 协商缓存(对比缓存)

当强制缓存失效(超过规定时间)时,就需要使用对比缓存,由服务器决定缓存内容是否失效。

流程上说,浏览器先请求缓存数据库,返回一个缓存标识。之后浏览器拿这个标识和服务器通讯。如果缓存未失效,则返回 HTTP 状态码 304 表示继续使用,于是客户端继续使用缓存;如果失效,则返回新的数据和缓存规则,浏览器响应数据后,再把规则写入到缓存数据库。

协商缓存有两组字段:

Last-Modified & If-Modified-Since

  1. 服务器通过Last-Modified字段告知客户端资源最后一次被修改的时间,例如:

    Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT
    
  2. 浏览器将这个字段的值和请求的内容存入缓存数据库中

  3. 下一次请求相同的资源时,找出“不确定是否过期的”缓存,在请求头中设置If-Modified-since字段为上次请求中存入缓存的Last-Modified字段的值;

  4. 服务端会将If-Modified-since和Last-Modified字段进行对比,相同的话,则资源未更新,返回304,客户端使用缓存;不同的话则表示资源已经更新,返回新的资源和新的缓存表示Last-Modified的值

  5. 缺陷:
    如果资源的更新速度是秒以下的速度更新,那么该缓存是不能被使用的,因为它的时间单位最低是以秒为单位
    如果文件是服务器动态生成的,那么更新时间永远是生成的时间,尽管文件可能没有变化,所以起不到缓存作用

E-tag & If-None-Match

  1. 为了解决上述问题,出现了E-tag和If-None-Match字段
  2. E-tag存储的是文件的标识(一般都是hash生成),服务端存储着文件的E-tag字段,之后的判断流程和If-Modified-Since相似,只不过判断资源是否过期是判断文件的E-tag字段是否变化(hash字段是否变化),把If-Modified-since变为了if-none-match。服务器同样进行比较;没变化则返回304;更新则返回200和新的缓存标识;
  3. E-tag的优先级高于Last-Modified

缓存小结

当浏览器请求资源时:

  • 调用Service-Worker的fetch()事件响应
  • 查看 menory-cache
  • 查看disk-cache,这里又细分为:
    强缓存:根据Expires和Cache-control两个字段进行判断;过期时间未失效,则使用强缓存,不请求服务器,状态码为200
    协商缓存:根据Last-Modified、If-Modified-Since和E-tag、If-None-match两组字段来判断;强制缓存失效,则使用协商缓存;根据服务器资源是否跟新返回304和200
  • 发送网络请求,等待响应
  • 把响应内容存入disk-cache(如果配置的响应头可以存储的话)
  • 把响应内容的引用存入memory-cache(无视响应头的hander信息设置)
  • 把响应内容存入Service-Worker的cache-storage;(如果service-worker的脚本调用了cache.put()方法)

浏览器行为(用户操作)

  1. 地址栏输入网址,回车:查找disk-cache中是否有匹配;有则使用,没有则发送网络请求
  2. 普通刷新(F5):因为Tab没有关闭,所以memory-cache可用,会被优先匹配使用,其次则匹配disk-cache
  3. 强制刷新(ctrl+f5):浏览器不使用缓存,直接返回最新资源和200;

参考文章

一文读懂前端缓存(这篇文章写的很好,推荐大家看一下)

深入理解浏览器的缓存级制(这篇文章也可以看一下)