前言
永远把别人对你的批评记在心里,别人的表扬,就把它忘了。Hello 大家好~!我是南宫墨言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,作用一样,但是它只在代理服务器中生效,且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中,供之后的解析执行操作,比如
preload
preload与preloader仅两个字母之差,他能显示指定预加载的资源,这些资源也会被放进memory cache中,比如
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):AB并没有关闭,因此
memory cache
是可用的,如果匹配的话就会被优先使用,其次才轮到disk cache
- 强制刷新(Crl + F5):浏览器不使用缓存,因此发送的请求头部均带有
Cache-Control: no-cache
(为了兼容,还带了Pragma: no-cache
),服务器直接返回200和最新内容