文章前言
永远把别人对你的批评记在心里,别人的表扬,就把它忘了。Hello 大家好~!我是南宫墨言QAQ
浏览器缓存是指浏览器在访问网页时将一些页面资源(如图像、样式表、脚本文件等)存储在本地计算机上的临时存储区域。当用户再次访问同一网页时,浏览器会首先检查缓存,如果找到了相应的资源,则直接从缓存中加载,而不是再次从服务器下载
缓存可以说是性能优化中简单高效的一种优化方式了。一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷
本文将从缓存类型、缓存位置、缓存过程、缓存机制、缓存策略以及实际场景应用等方面来剖析浏览器缓存机制
浏览器缓存机制
观看到文章最后的话,如果觉得不错,可以点个关注或者点个赞哦!感谢~❤️
文章主体
感谢各位观者的耐心观看,浏览器缓存正片即将开始,且听南宫墨言QAQ娓娓道来
缓存类型
缓存类型可以分为强缓存和协商缓存。强缓存是不需要发送HTTP请求,而协商缓存是需要发送HTTP请求的。也就是说,在发送HTTP请求之前,浏览器会先检查强缓存,如果命中就直接返回,否者进入协商缓存阶段
强缓存
不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的Network选项中可以看到该请求返回 200 的状态码,并且Size显示from disk cache或from memory cache
浏览器检查强缓存主要是判断这两个字段:Expires(有效期,HTTP/1.0的产物)和 Cache-Control(缓存管理,HTTP/1.1的产物)
Expires
Expires表示缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点,存在于服务端返回的响应头中,告诉浏览器在这个过期时间之前可以直接从缓存里面获取数据,无需再次请求。比如下面这样:
Expires: Tue Jul 11 2023 18:00:00 GMT+0800
表示这个资源在2023年7月11日18点过期,到了过期时间就需要重新向服务器请求
但是,当服务器时间和浏览器时间不一致(比如手动修改本地时间),那么就会导致缓存失效。因为这个缺陷,所以在HTTP1.1中被抛弃了
Cache-Control
抛弃了Expires之后, HTTP/1.1采用了Cache-Control这个重要的规则,它和Expires本质的不同在于它并没有采用具体的过期时间点这个方式,而是采用过期时长来控制缓存,对应的字段是max-age
Cache-Control:max-age=3600
代表这个响应返回后在 3600 秒,也就是一个小时之内可以直接使用缓存
Cache-Control不仅仅有max-age 这一个属性, 其实它有很多的用法, 你甚至可以采用组合的方式:
Cache-Control: public, max-age=3600
上面👆用法的意思是响应可以被任何对象(客户端, 代理服务器等)缓存, 且过期时长为1小时
因为一个请求经历的不仅仅是客户端(浏览器)和目标服务器,它中间有可能会经过不同的代理服务器,所以我们通过组合不同指令限制其使用场景,下面列举下Cache-Control中常用的指令:
Cache-Control指令
public 表示客户端和代理服务器都可以缓存。响应可以被中间任何的一个节点缓存,在Browser -> proxy1 -> proxy2 -> Server这个请求过程中,中间的代理(proxy)可以缓存资源,当Browser再次请求这个资源的时候,浏览器就会直接到proxy1中拿缓存的东西而不必再经过proxy2去取得
private 表示响应只可以被客户端缓存,是Cache-Control的默认取值。响应不允许中间节点缓存,在Browser -> proxy1 -> proxy2 -> Server这个请求过程中,中间的代理(proxy)不可以缓存资源,当Browser再次请求这个资源的时候,proxy会把Server返回的数据发送给Browser,做好请求转发,而不是自己缓存数据
max-age 表示缓存的资源在多久之后过期。max-age=3600表示在1小时后缓存的资源失效,需要重新发起HTTP请求
s-maxage 覆盖max-age,和max-age作用一样,但是它只在代理服务器中生效,且s-maxage的优先级高
no-store 表示资源都不会被缓存,既不进行强缓存,也不进行协商缓存
no-cache 表示不进行强缓存验证,发送HTTP请求,直接进入协商缓存阶段
max-stale 表示能容忍的最大过期时间。max-stale=30表示30秒内,即使缓存过期,客户端也使用该缓存
min-fresh 表示能容忍的最小新鲜度。min-fresh=30表示希望在30秒内获取最新的响应
基于以上这些指令,我们可以对其进行不同的组合以达到不同的目的,实现不同的效果
Cache-Control
从图中可以看出,我们可以基于这些指令进行不同的组合,以达到不同的目的,实现不同的效果。
当然,还存在一种情况,当资源缓存时间超时了,也就是强缓存失效了,就可以进入第二级屏障 - 协商缓存
协商缓存
会向服务器发起请求的缓存。协商缓存就是在强缓存失效的情况下,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否是使用缓存的过程,主要有以下两种情况:
- 协商缓存生效,返回304和 Not Modified
协商缓存生效
- 协商缓存失效,返回200和请求结果
协商缓存失效
上面提到的缓存标识有两个:Last-Modified和ETag,这两者各有优劣,并不存在谁对谁有绝对的优势,下面将对这两个标识进行剖析
Last-Modified
Last-Modified表示的是资源的最后修改时间,这也是协商缓存判断方式的一种,它会配合If-Modified-Since字段使用
使用Last-Modified进行协商缓存会经过以下几个步骤:
协商缓存Last-Modified
- 浏览器第一次向服务器请求这个资源
- 服务器在返回这个资源的时候,会在
response header中添加Last-Modified这个标识, 值为该资源在服务器上最后的修改时间 - 浏览器接收到缓存资源和header
- 当下次浏览器再次请求这个资源的时候,检测到有
Last-Modified这个标识,就会在请求头中添加If-Modified-Since这个标识,值为Last-Modified - 服务器再次接收到该资源的请求,则根据
If-Modified-Since与服务器中的这个资源的最后修改时间做对比 - 对比结果相同则返回304和一个空的响应体,告诉浏览器从自己的浏览器缓存中取
- 对比结果不同(
If-Modified-Since小于服务器资源最后修改时间),则表示资源被修改了,那么就返回200和最新的资源,当然包括最新的Last-Modified
但是Last-Modified存在一些弊端
- 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成
Last-Modified被修改,服务端不能命中缓存导致发送相同的资源 - 因为
Last-Modified只能以秒计时,如果某个文件在1秒内被修改了很多次,那么这时候的Last-Modified并没有体现出修改了,此时服务端不会返回正确的资源
既然根据文件修改时间来决定是否缓存尚有不足,能否可以直接根据文件内容是否修改来决定缓存策略呢?所以在HTTP/1.1的时候出现了ETag和If-None-Match
ETag
ETag与Last-Modified的原理差不多,不过它不是根据资源的最后修改时间来判断的,而是通过一个唯一的标识,这个标识是根据当前的文件内容生成的,只要里面的内容有改动,这个值就会改变,它会配合If-None-Match字段使用
使用ETag进行协商缓存会经过以下几个步骤:
协商缓存ETag
- 浏览器第一次向服务器请求这个资源
- 服务器在返回这个资源的时候,会在
response header中添加ETag这个标识 - 浏览器接收到缓存资源和header
- 当下次浏览器再次请求这个资源的时候,浏览器会在请求头中添加
If-None-Match,值为刚刚缓存的ETag标识 - 服务器再次接收到该资源的请求,则根据
If-None-Match与服务器中的该资源自身做对比 - 对比结果相同则返回304,告诉浏览器从自己的浏览器缓存中取
- 对比结果不同,则表示资源被修改了,那么就返回200和最新的资源,当然包括最新的
ETag
两者之间对比
-
在精度上,
ETag要优于Last-Modified,Last-Modified只能以秒计时,如果某个文件在1秒内被修改了很多次,那么这个时候Last-Modified并没有体现出修改了,但ETag每次都会改变,确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致 -
在性能上,
ETag要逊于Last-Modified,毕竟Last-Modified,只需要记录时间,而ETag则需要服务器通过算法来计算出一个hash值 -
在优先级上,服务器校验优先考虑
ETag
总结协商缓存:准确度上ETag更强;性能上Last-Modified更好;两者都支持的话,ETag优先级更高
缓存位置
在上面已经介绍完了缓存的类型,接下来我们看看缓存位置的概念。之前说过,如果命中了强缓存或者服务器返回了304之后,要浏览器从缓存中获取资源,那么这些缓存具体是存储在哪里呢?
缓存位置从优先级上依次划分为以下四种:Service Worker、Memory Cache、Disk Cache、Push Cahe,下面将会对这四种缓存位置进行剖析
Service Worker
Service Worker是运行在浏览器背后的独立线程,由于它脱离了浏览器的窗体,所以无法直接访问DOM。虽然如此,但它仍然能帮助我们完成很多有用的功能,比如离线缓存、消息推送和网络代理等功能。其中离线缓存就是Service Worker Cache
简单的来说,它具有以下几个特点:
- 借鉴了
Web Worker的思路 - 使用
Service Worker的话,传输协议必须为HTTPS,因为Service Worker中涉及到请求拦截,所以需要用HTTPS协议来保证安全 - 与浏览器其它内建缓存机制不同,它可以让我们自由的控制缓存哪些文件,如何匹配缓存、如何读取缓存,并且缓存是持续性的
Service Worker是PWA的重要实现机制
Memory Cache
Memory Cache是内存中的缓存,主要存储的是当前页面已经抓取到的资源,比如页面上已经下载的样式、脚本、图片等
简单的来说,它具有以下几个特点:
- 读取效率快,当缓存持续时间短,会随着进程的释放而释放(一旦关闭Tab页面,就被释放了,还有可能在没有关闭之前,排在前面的缓存就失效了,比如一个页面的缓存占用了超级多的内存)
- 几乎所有的请求资源都能进入
memory cache,细分来说主要分为preloader和preload - 从
memory cache读取缓存时,并不关心返回资源的HTTP响应头中Cache-Control中max-age、no-cache等头部配置,除非设置了no-store,同时资源的匹配也并非仅仅是对URL做匹配,还可能对Content-Type、CORS等其他特性做校验
preloader
preloader是页面优化的常见手段之一,它的作用主要是用于在浏览器打开一个网页的时候,能够一边解析执行js/css,一边去请求下一个资源,而这些被preloader请求来的资源就会被放入memory cache中,供之后的解析执行操作,比如:
<link rel="prefetch">
preload
preload与preloader仅两个字母之差,它能显示指定预加载的资源,这些资源也会被放进memory cache中,比如:
<link rel="preload">
Disk Cache
Disk Cache也叫做HTTP Cache,是存储在硬盘上的缓存,所以它是持久存储,是实际存在于文件系统的
从存储效率上说,它比内存缓存慢,但是优势在于什么都能存储,存储容量更大,并且存储时长更长
在所有浏览器缓存中,Disk Cache是覆盖面积最大的,它会根据前面我们提到的HTTP header中的缓存字段来判断哪些资源需要缓存,哪些资源不需要请求直接使用,哪些已经过期了需要重新请求获取
若是命中了缓存之后,浏览器会从硬盘中直接读取资源,虽然没有从内存中读取的快,但比网络缓存来的快
强缓存和协商缓存也是属于Disk Cache,它们最终都存储在硬盘里
Memory Cache和Disk Cache各有优劣,那么浏览器如何决定将资源放进内存还是硬盘呢,主要策略如下:
- 对于大文件来说会被丢到硬盘中存储,反之则存储在内存中
- 当前系统使用率高的话,文件优先存储进硬盘
Push Cahe
Push Cahe(推送缓存),它是浏览器缓存的最后一道防线,当以上三种缓存都没有命中的时候,它才会被使用
它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存的时间也很短暂,在Chorome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令
由于它是Http/2中的内容,因此在国内不是很普及,这里贴上一个比较好的总结:
- 所有的资源都能被推送,并且能够被缓存,但Edge和Safari浏览器支持相对比较差
- 可以推送no-cache和no-store的资源
- 一旦链接被管理,
Push Cahe就被释放 - 多个页面可以使用同一个HTTP/2的连接,也就可以使用同一个
Push Cahe。主要还是依赖浏览器的实现而定,出于对性能的考虑,有的浏览器会对相同域名但不同的tab标签使用同一个HTTP连接 Push Cahe的缓存只能被使用一次- 浏览器可以拒绝接受已经存在的资源推送
- 你可以给其他域名推送资源
缓存过程
根据上面的介绍,我们知道了缓存类型和缓存位置,那么浏览器具体的缓存行径是怎么样的呢?
浏览器与服务器通信方式为应答模式,即浏览器发起HTTP请求,服务器响应该请求。那么浏览器怎么确定一个资源该不该缓存,如何去缓存呢?
浏览器发起HTTP请求到获得请求结果的过程可以分为以下几个步骤:
HTTP请求过程
- 浏览器第一次发起HTTP请求,在浏览器缓存中没有发现请求的缓存结果和缓存标识
- 因此向服务器发起HTTP请求,获得该请求的结果还有缓存规则(
Last-Modified或ETag) - 浏览器把响应内容存入
Disk Cache,把响应内容的引用存入Memory Cache - 把响应内容存入
Service Worker的Cache Storage(如果Service Worker的脚本调用了cache.put())
下一次请求相同资源的时候:
- 调用
Service Worker的fetch事件响应 - 查看
memory cache - 查看
disk cache,这里细分为:
- 有强缓存且未失效,则使用强缓存,不请求服务器,返回的状态码都是200
- 有强缓存且已失效,使用协商缓存判断,是304还是200(读取缓存还是重新获取)
缓存机制
强制缓存优先于协商缓存进行,若强者缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified/If-Modified-Since和ETag/If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回200、资源和缓存标识,再存入浏览器缓存中,生效则返回304,继续使用缓存,具体流程如下图:
缓存的机制
如果什么缓存策略都没设置,那么浏览器会怎么处理呢?
对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的Date减去Last-Modified值的10%作为缓存时间
缓存策略的实际应用场景
在实际使用上来说,我们怎么使用缓存策略呢?对此我们可以针对资源是否频繁变动进行剖析
频繁变动的资源
对于频繁变动的资源,首先需要使用Cache-Control: no-cache
Cache-Control: no-cache
这样使得浏览器每次都请求服务器,然后配合ETag或者Last-Modified来验证资源是否有效。
这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小
非频繁变动的资源
对于非频繁变动的资源,我们会给Cache-Control: max-age=3153600
Cache-Control: max-age=3153600
通过给Cache-Control配置一个很大的max-age,这样子浏览器之后请求相同的URL会命中强缓存。而为了解决更新的问题,就需要在文件名或路径中添加hash、版本号等动态字符,之后通过更改动态字符,就能达到更改引用URL的目的,让之前的强制缓存失效(其实并没有立即失效,只是不再使用了而已)
用户行为对浏览器缓存的影响
用户行为对浏览器缓存的影响指的是用户在浏览器如何操作时,会触发怎样的缓存策略,主要行为有三种:
- 打开网页,地址栏输入地址:查找
disk cache中是否有匹配,如果有就使用,反之就发送网络请求 - 普通刷新(F5):TAB并没有关闭,因此
memory cache是可用的,如果匹配的话就会被优先使用,其次才轮到disk cache - 强制刷新(Crl + F5):浏览器不使用缓存,因此发送的请求头部均带有
Cache-Control: no-cache(为了兼容,还带了Pragma: no-cache),服务器直接返回200和最新内容
浏览器缓存面试题
这里留一些浏览器缓存的相关面试题,有兴趣的小伙伴可以研究下,在评论区探讨
- 浏览器缓存的好处是什么?
- 浏览器缓存会带来什么问题呢?
- 如果强缓存没有过期的话,但是文件发生了修改,那怎么更新这个缓存呢?
参考文章
南宫墨言QAQ在此非常感谢以下几位优秀的博主提供他们关于浏览器缓存的优秀文章,感谢~❤️
结尾营业
看官都看到这了,如果觉得不错,可不可以不吝啬你的小手手帮忙点个关注或者点个赞