1. 前言
一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷,提高网页的整体加载速度。
对于一条网络请求来说, 一般会经历浏览器发送请求==>>后端处理请求==>>浏览器响应请求。 如果不使用缓存的话每条请求都会经历这个过程,若重复请求相同资源,会对服务器资源造成浪费,服务器重复读取资源,发送给浏览器,浏览器重复下载, 造成不必要的等待和消耗。而浏览器缓存发生在浏览器发送请求和浏览器响应请求阶段,直接使用缓存就可以避免上面请求过程的重复发生。
下面将通过缓存策略、缓存位置以及使用场景展开

2. 缓存读取的过程
浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识,如果找到缓存验证时效性,如果缓存有效则使用该缓存并且状态码为200(这个过程为强缓存);如果缓存无效,浏览器会发送请求并且携带缓存标识,服务器会依据请求携带的缓存标识决定是否使用缓存,使用缓存返回304和Not Modified,此时浏览器使用缓存,若文件被修改则会返回200和文件内容,浏览器放弃使用缓存,使用请求返回的内容(这个过程为协商缓存)。

3. 强缓存
强缓存:不会向服务器发送请求,直接从缓存中读取资源,在浏览器控制台的Network选项中可以看到该请求返回200的状态码,并且Size显示from disk cache或from memory cache。强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control。
3.1 Expires
缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。
Expires 是 HTTP/1.0 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。
Expires: Wed, 22 Oct 2018 08:41:00 GMT表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。
3.2 Cache-Control
Cache-Control是在http/1.1中添加的,为了解决Expires存在的问题,该字段与Expires的缓存思路相同,都是设置了一个过期时间,不同的是max-age设置的是相对开始缓存时间开始往后多久,因此不存在受日期不准确情况的影响。Cache-Contro可以在请求头或者响应头中设置,并且可以组合使用多种指令:
| 指令 | 指令作用 |
|---|---|
| public | 表示响应可以被客户端和代理服务器缓存 |
| private | 表示响应只能被客户端缓存,默认值 |
| max-age=30 | 缓存30s后过期 |
| s-maxage | 覆盖max-age,作用一样,只在代理服务器中生效 |
| no-store | 所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存 |
| no-cache | 资源被缓存,但是不使用(强缓存失效),请求会验证资源是否过期 |
| max-stale=30 | 30s内,即使缓存过期,也是用该缓存 |
| min-fresh=30 | 希望在30s内获取最新的响应 |
3.3 Expires和Cache-Control两者对比
-
- Expires 是http1.0的产物,Cache-Control是http1.1的产物,
-
- 两者同时存在的话,Cache-Control优先级高于Expires;在某些不支持HTTP1.1的环境下,Expires就会发挥用处。所以Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法。
强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容是否已经发生了更新呢?此时我们需要用到协商缓存策略。
4. 协商缓存
协商缓存就是强缓存失效时,浏览器请求会携带缓存标识到服务器,服务器根据该标识决定是否使用缓存,主要有两种情况:
- 协商缓存生效,返回304和Not Modified
- 协商缓存失效,返回200和请求结果
协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag 。
4.1 Last-Modified和If-Modified-Since
浏览器在第一次访问资源时,服务器返回资源的同时,在response header中添加 Last-Modified的header,值是这个资源在服务器上的最后修改时间,浏览器接收后缓存文件和header;
Last-Modified: Mon, 25 May 2020 08:51:24 GMT
浏览器下一次请求这个资源,浏览器检测到有Last-Modified这个header,于是添加If-Modified-Since这个header,值就是Last-Modified中的值;服务器再次收到这个资源请求,会根据 If-Modified-Since中的值与服务器中这个资源的最后修改时间对比,如果没有变化,返回304和空的响应体,直接从缓存读取,如果If-Modified-Since的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和200
但是 Last-Modified 存在一些弊端:
- 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
- 因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源
既然根据文件修改时间来决定是否缓存尚有不足,能否可以直接根据文件内容是否修改来决定缓存策略?所以在 HTTP / 1.1 出现了 ETag 和If-None-Match
4.2 ETag
ETag是对资源的特殊标识
Etag: W/"e563df87b65299122770e0a84ada084f"
请求该资源成功之后,将返回的ETag存入If-None-Match字段中(浏览器自动记录了该字段信息),同样在请求资源时传递给服务器,服务器查询该编码对应的资源有无更新,无更新返回304状态,更新返回200并重新请求。
4.3 ETag如何计算
ETag是针对某个文件的特殊标识,服务器默认采用SHA256算法生成。也可以采用其他方式,保证编码的唯一性即可。
4.4 两者之间对比
- 首先在精确度上,Etag要优于Last-Modified。 Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。
- 第二在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
- 第三在优先级上,服务器校验优先考虑Etag
5 缓存位置
前面我们已经提到,当强缓存命中或者协商缓存中服务器返回304的时候,我们直接从缓存中获取资源。那这些资源究竟缓存在什么位置呢? 浏览器中的缓存位置一共有四种,按优先级从高到低排列分别是:
- Service Worker
- Memory Cache
- Disk Cache
- Push Cache
5.1 Service Worker
Service Worker借鉴了 Web Worker的 思路,即让 JS 运行在主线程之外,由于它脱离了浏览器的窗体,因此无法直接访问DOM。虽然如此,但它仍然能帮助我们完成很多有用的功能,比如离线缓存、消息推送和网络代理等功能。其中的离线缓存就是 Service Worker Cache。
Service Worker 同时也是PWA的重要实现机制,关于它的细节和特性,我们将会在后面的 PWA 的分享中详细介绍。
5.2 Memory Cache 和 Disk Cache
Memory Cache指的是内存缓存,从效率上讲它是最快的。但是从存活时间来讲又是最短的,当渲染进程(tab页)结束后,内存缓存也就不存在了。
Disk Cache就是存储在磁盘中的缓存,从存取效率上讲是比内存缓存慢的,但是他的优势在于存储容量和存储时长。稍微有些计算机基础的应该很好理解,就不展开了。
好,现在问题来了,既然两者各有优劣,那浏览器如何决定将资源放进内存还是硬盘呢?主要策略如下:
- 比较大的JS、CSS文件会直接被丢进磁盘,反之丢进内存
- 内存使用率比较高的时候,文件优先进入磁盘
5.3 Push Cache
即推送缓存,这是浏览器缓存的最后一道防线。它是 HTTP/2 中的内容,虽然现在应用的并不广泛,但随着 HTTP/2 的推广,它的应用越来越广泛。
6 实际场景应用缓存策略
6.1 频繁变动的资源
可以使用Cache-Control: no-cache每次都向服务器发送请求并携带Etag或Last-Modified验证资源是否发生变化。
6.2 不经常变动的资源
可以设置Cache-Control: max-age=2592000(30天或更长的时间间隔),这样浏览器之后请求相同的 URL 会命中强制缓存。为了避免文件更新不能及时更新的问题,可以在文件名中添加hash、版本号等动态字符,从而改动资源的URL,达到更改的文件能及时生效。
7 用户行为对浏览器缓存影响
用户输入地址栏,查找disk cache是否匹配, 不匹配发送请求。普通刷新(Mac:command+R,windows:F5),优先使用memory cache,其次使用disk cache。强制刷新(Ctrl+F5)(Mac:command+shift+R,windows:Ctrl+F5),不使用缓存。