前端八股文之浏览器缓存

什么是浏览器缓存

浏览器缓存,又称 HTTP 缓存;浏览器将用户请求过的静态资源(html、css、js、图片等),存储到电脑本地磁盘或内存中,当浏览器再次访问时,就可以直接从本地加载而不需要再去服务端请求了,这样一种行为就是浏览器缓存;

浏览器缓存是浏览器的一种机制,即浏览器缓存机制,其机制是根据 HTTP 报文的缓存标识进行的,所以也叫 HTTP 缓存机制;

为什么要使用缓存

因为它速度快,官方一点的说法:

  1. 减少了冗余的数据传输,节省了网络流量;
  2. 减少了服务器的负担,提高了网站的性能;
  3. 加快了客户端加载页面的速度;

缓存的本质是将部分的数据使用另一种存取速度更快的介质存储,使系统更快的操作和响应;

缓存的资源去哪儿了

从缓存的位置上来说分为四种,且各自有优先级:

  • Service Worker
  • Memory Cache
  • Disk Cache
  • Push Cache

Service Worker

Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。Service Worker 的缓存与浏览器其他内建的缓存机制不同,Service Worker 不遵循任何常规规则,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的,从某种意义上讲,这是不可预测的,它只遵循 Web 开发人员设置的规则,所以它不在我们今天主题的讨论范畴;

Memory Cache

即内存缓存,就是将资源缓存到内存中,等待下次访问时不需要重新下载资源,而直接从内存中获取。

读取内存中的数据比磁盘要快的多,但是缓存时效性很短,会随着进程的释放而被清除;当我们关闭 Tab 页面时,内存中的缓存也会被清除;虽然读取内存数据比磁盘要快,但并不意味着我们可以把所有的数据都放到内存中去,因为内存比硬盘的容量要小的多,操作系统都要精打细算的用,所以能让我们使用的内存必然不多;

内存中有一块重要的缓存资源是 preloader(预加载) 相关指令下载的资源,例如 ;preloader 相关指令是页面优化的常见手段之一,它可以一边解析 js/css 文件,一边下载另一个资源;

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

Disk Cache

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

磁盘缓存会根据 HTTP 报文中的 Cache-Control 字段的值判断哪些资源需要缓存,缓存时长,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求;即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据;

这也是同一个公司的不同的项目,都用到了 jQuery 库,我们都引用同一个地址的原因,不仅仅是为了方便管理,也是为了提升网站性能和网页加载速度;

那么,浏览器会把哪些文件缓存至内存中,哪些文件缓存到磁盘中呢?

  • 对于大文件来说,大概率是不存储在内存中的,反之优先;
  • 当前系统内存使用率高的话,文件优先存储进硬盘;
  • 一般脚本、字体、图片会存在内存当中;
  • 一般非脚本会存在内存当中,如 CSS 等;

Push Cache

即推送缓存,Push Cache 是 HTTP/2 中的内容,它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在 Chrome 浏览器中只有5分钟左右,同时它也并非严格执行 HTTP 报文中的缓存指令,所以它也不在我们今天主题的讨论范畴;

优先级:Service Worker > Memory Cache > Disk Cache > Push Cache

如果以上四种缓存都没有命中的话,那么只能发起 HTTP 请求来获取资源了;

一般情况下我们所说的浏览器缓存都是存储在内存和硬盘中的,即 Memory Cache 和 Disk Cache。

缓存过程分析

浏览器与服务器通信的方式为应答模式,即浏览器发起 HTTP 请求 – 服务器响应该请求;那么浏览器第一次向服务器发起该请求后拿到请求结果,会根据 HTTP 响应报文中的缓存标识,决定是否缓存结果,是则将请求结果和缓存标识存入浏览器缓存中,简单的过程如下图:

Cache.png

由上图我们可以知道:

  1. 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识;
  2. 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中;

以上两点结论就是浏览器缓存机制的关键,它确保了每个请求的缓存写入与读取,只要我们再理解浏览器缓存的使用规则,那么所有的问题就迎刃而解了,接下来我们将围绕着这点进行详细分析;

为了方便大家理解,我们根据是否需要向服务器重新发起 HTTP 请求将缓存过程分为两个部分,分别是强缓存和协商缓存 ;

缓存规则

浏览器缓存规则,又称浏览器缓存策略;我们根据是否需要向服务器重新发起 HTTP 请求将其分为强缓存协商缓存

强缓存

简单粗暴,如果资源没过期,就取缓存,如果资源过期了,就请求服务器;主要有以下三种情况:

  1. 不存在该缓存结果和缓存标识,强缓存失效,则直接向服务器发起请求(跟第一次发起请求一致),如图:

Cache1.png

  1. 存在该缓存结果和缓存标识,但是结果已失效,强缓存过期,则使用协商缓存,如图:

Cache2.png

  1. 存在该缓存结果和缓存标识,且结果还没有失效,强缓存生效,直接返回该结果,如图:

Cache3.png

资源是否过期主要是看 HTTP 响应报文中的 Expires 和 Cache-Control 的字段值;

Expires
Expires 是 HTTP/1.0 控制网页缓存的字段,其值为服务器返回该请求结果的缓存到期时间,即再次发送 HTTP 请求时,如果客户端的时间小于 Expires 的值时,直接使用缓存结果;Expires = 请求时间 + 缓存时长;

Expires 是 HTTP/1.0 的字段,但现在浏览器默认使用的是 HTTP/1.1;在 HTTP/1.1 中,使用 Cache-Control 替代了 Expires,原因在于 Expires 控制缓存的原理是使用客户端的时间与服务端返回的时间做对比,如果客户端与服务端的时间由于某些原因(时区不同,或者客户端和服务端有一方的时间不准确)发生误差,那么强缓存直接失效,那么强缓存存在的意义就没有了;

Cache-Control
Cache-Control 字段值较常用的有以下几种:

  • public:所有内容都将被缓存(客户端和代理服务器都可缓存);
  • private:所有内容只有客户端可以缓存,Cache-Control 的默认取值;
  • no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定;

即不使用强缓存规则做前置验证,直接请求服务器由协商缓存来决定是否使用该缓存结果;需要注意的是,no-cache 这个名字有一点误导,设置了 no-cache 之后,并不是说浏览器就不再缓存资源,只是浏览器在使用缓存资源时,需要先确认一下资源是否还跟服务器保持一致;

  • no-store:所有内容都不会被缓存,既不使用强缓存,也不使用协商缓存;
  • max-age=xxx (xxx is numeric):单位秒(s),缓存内容将在 xxx 秒后失效;
  • s-maxage:单位秒(s),同 max-age,只在代理服务器中生效(比如 CDN 缓存);比如当 s-maxage=60 时,在这60秒中,即使更新了 CDN 的内容,浏览器也不会进行请求;max-age 用于普通缓存,而 s-maxage用于代理缓存;s-maxage 的优先级高于 max-age,如果存在 s-maxage,则会覆盖掉 max-age 和 Expires;

Cache4.png

优先级
当 Cache-Control 和 Expires 两者同时存在时,Cache-Control 优先级高于 Expires;在某些不支持 HTTP/1.1 的环境下,Expires 就会发挥作用,现阶段它的存在只是一种兼容性写法;

协商缓存

触发条件(任意一个):

  • Cache-Control 的值为 no-cache(不强缓存);
  • max-age 过期了(强缓存,资源已过期);

也就是说,只要没有命中强缓存,浏览器就会携带缓存标识向服务器发起请求进行协商缓存(no-store 除外),由服务器根据缓存标识决定是否使用缓存的过程;

协商缓存有两种情况:

  1. 命中协商缓存,如图:

Cache5.png

  1. 协商缓存失效了,如图:

Cache6.png

同样,协商缓存是否生效主要是看 HTTP 报文中的 Last-Modified/If-Modified-SinceETag/If-None-Match 的字段值;

Last-Modified 和 If-Modified-Since

  • Last-Modified 是服务器响应请求时,返回该资源文件在服务器最后被修改的时间;
  • If-Modified-Since 则是浏览器进行协商缓存,发起请求时,携带上次请求返回的 Last-Modified 值;

服务器收到该请求后,通过此字段值与该资源在服务器的最后被修改时间做对比,若服务器的资源最后被修改时间大于 If-Modified-Since 的字段值,则重新返回资源,状态码为200;否则返回状态码304,代表资源无更新,可继续使用缓存资源;

ETag 和 If-None-Match

  • ETag 是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成);只要资源有变化,ETag 就会重新生成,类似 MD5;
  • If-None-Match 则是浏览器进行协商缓存,发起请求时,携带上次请求返回的唯一标识 ETag 值;

服务器收到该请求后,通过此字段值与该资源在服务器的 ETag 值做对比,一致则返回状态码304,代表资源无更新,继续使用缓存资源;不一致则重新返回资源文件,状态码为200;

If-Modified-Since 和 If-None-Match 就是进行协商缓存,发起 HTTP 请求时所携带的缓存标识;

优先级
ETag/If-None-Match 的优先级比 Last-Modified/If-Modified-Since 高,这是因为 Last-Modified/If-Modified-Since 存在一些弊端:

  • 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源;
  • 因为 Last-Modified 的单位是秒,如果在不可感知的时间内修改完成资源,那么服务端会认为资源还是命中了,不会返回最新的资源;

所以在 HTTP/1.1 中新增了 ETag/If-None-Match 作为补充;

两者对比

  • 性能上,Last-Modified/If-Modified-Since 只需要记录时间,ETag/If-None-Match 需要服务器通过算法计算出一个 Hash 值,所以在性能上前者要优于后者;
  • 精度上,Last-Modified/If-Modified-Since 的单位是秒,比如某个文件在 1 秒内修改了数次,那么 Last-Modified/If-Modified-Since 是无法感知和体现的;ETag/If-None-Match 则每次都会改变以确保精度;

题外话

  1. 不同的代理服务,ETag 的生成因素不同:
    • Apache 的默认 ETag 值总是由文件的索引节点(Inode)、大小(Size)、最后修改时间(MTime)决定;
    • Nginx 中的 ETag 由 Last-Modified 与 Content-Length 组成,而 Last-Modified 又由 MTime 组成;
    • Express 框架采用 ETag 库生成;Koa 框架的 ETag 插件底层也是 Express 的 ETag;
  2. 我们以上讨论的都是单台服务器的情况,如果服务端应用了负载均衡策略,因为各个服务器的 inode 不同,因此对应生成的 ETag 也是不一样的,有可能 Last-Modified 也是不一致的;感兴趣可以了解一下这种情况的解决方案;
  3. ETag 是可以被关闭的,这时候服务器将使用 If-Modified-Since 参数来判断资源是否有更新;

总结

  • 强缓存优先于协商缓存进行,若强缓存(Expires 和 Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified/If-Modified-Since 和 ETag/If-None-Match);
  • 协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回状态码200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回状态码304,继续使用缓存;
缓存类型 获取资源方式 状态码 是否向服务器发送请求
强缓存 从缓存获取 200 (memory cache/disk cache) 否,直接从缓存中取
协商缓存 从缓存获取 304 是,通过服务器来告知缓存是否可用

Cache7.png

应用场景

  1. 频繁变动的资源:Cache-Control: no-cache

虽然不能节省请求数量,但是能显著减少响应数据大小;

  1. 不常变化的资源:Cache-Control: max-age=31536000

主要用于引用的一些第三方库/插件;如果要更新这类资源,就需要在文件名(或者路径)中添加 hash 值,版本号等动态字符,之后更改动态字符,从而达到更改引用 URL 的目的,让之前的强缓存失效(其实并未立即失效,只是不再使用了而已);
可以看一下这篇知乎大佬的回答:大公司里怎样开发和部署前端代码?

在不设置 Cache-Control 的情况下,浏览器会根据自身的情况去取舍相关的缓存,可以看一下这个回答贴;如果大家在服务器配置过程中发现,自己没有配置任何的缓存信息但是浏览器却缓存了资源就不用惊讶;

如何管理缓存

前边我们介绍了强缓存的规则,在实际应用中我们会碰到需要强缓存的场景和不需要强缓存的场景,通常有2种方式来设置是否启用强缓存:

  1. 通过代码的方式,在服务器返回的响应中添加 Expires 和 Cache-Control Header;
  2. 通过配置服务器的方式,让服务器在响应资源的时候统一添加 Expires 和 Cache-Control Header;

由于在开发的时候不会专门去配置强缓存,而浏览器又默认会缓存图片,css 和 js 等静态资源,所以开发环境下经常会因为强缓存导致资源没有及时更新而看不到最新的效果,解决这个问题的方法有很多,常用的有以下几种:

  1. 直接 Ctrl + F5,这个办法能解决页面直接引用的资源更新的问题;
  2. 使用浏览器的隐私模式开发;
  3. 如果用的是 Chrome,可以 F12 在 network 面板中把缓存给禁掉(勾上 Disable cache 选项,这是个非常有效的方法);
  4. 在开发阶段,在文件名(或者路径)中添加 hash 值,版本号等动态字符;
  5. 如果资源引用的页面,被嵌入到了一个 iframe 里面,可以在 iframe 的区域右键单击 [重新加载框架](以 Chrome 为例);
  6. 如果缓存问题出现在 Ajax 请求中,最有效的解决办法就是 Ajax 的请求地址追加随机数;

用户行为对缓存的影响

用户操作 强缓存 协商缓存
地址栏回车 有效 有效
页面链接跳转 有效 有效
新开窗口 有效 有效
前进/后退 有效 有效
F5 刷新 无效 有效
Ctrl + F5 强制刷新 无效 无效

参考文章

发表评论
* 昵称
* Email
* 网址
* 评论