前几天某人问我了个问题,说了个大概,回头我又查阅资料,了解网上大牛们的文章后进行一下小总结。

start

经常访问某网站页面,有的页面加载快,有的跟蜗牛一样慢,为什么同一个网站会有如此大的差别呢,我们排除网络因素来分析下,访问页面无非就是请求服务器返回特定资源,如果本地有了资源,肯定比访问服务器的要快,本地的这个资源可以理解为:缓存。
image.png
上图形象的展现了缓存访问机制。打开控制台,实际操作一下,再看一下访问的页面:
image.png

缓存是浏览器一个提高页面交互体验的重要工具之一,根据其存储位置不一样大概分为这么几种:

这四种都是缓存策略,且是按顺序访问并执行,来具体看下细节。

一、Service Worker

service work是什么?
它是一段运行在浏览器后台进程里的脚本,独立于当前页面。
service work有什么作用?
1.网络代理,转发请求,伪造响应
2.离线缓存
3.消息推送
4.后台消息传递
如何查看service work?
1.通过命令 chrome://serviceworker-internals 可以查看;
2.通过devtools-Application查看。

效果一样,界面不同,如下图:
方式一:
image.png

方式二:
image.png

Service Worker(仅简单说跟缓存相关)的使用是有限制的,必须运行在htts协议下,因为它涉及到请求拦截,所以通过https协议保障其安全性。Service Worker的缓存与浏览器内建的缓存策略不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
下图显示了可用的Service Worker事件的摘要
image.png

Service Worker实现缓存分为三个步骤:

1.首先注册 Service Worker
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('./sw-test/sw.js', {scope: './sw-test/'})
  .then((reg) => {
    // registration worked
    console.log('Registration succeeded. Scope is ' + reg.scope);
  }).catch((error) => {
    // registration failed
    console.log('Registration failed with ' + error);
  });
}
2.注册后,增加时间侦听
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('v1').then((cache) => {
      return cache.addAll([
        './sw-test/',
        './sw-test/index.html',
        './sw-test/style.css',
        './sw-test/app.js',
        './sw-test/image-list.js',
        './sw-test/star-wars-logo.jpg',
        './sw-test/gallery/',
        './sw-test/gallery/bountyHunters.jpg',
        './sw-test/gallery/myLittleVader.jpg',
        './sw-test/gallery/snowTroopers.jpg'
      ]);
    })
  );
});

监听到 install 事件以后就可以缓存需要的文件,如上图的目录及静态资源。
在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件。

3.如果没有缓存,则调用fetch函数请求数据。

关于fetch也很好理解,如下图:
image.png

fetch每当获取任何资源,都会触发一个事件,然后respondWith()在事件上调用方法来劫持我们的HTTP响应,并使用您自己的魔法对其进行更新。因为有劫持,所以会不安全,就需要在https协议下运行。

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
  );
});
Service Worker如何更新缓存呢?
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('v2').then((cache) => {
      return cache.addAll([
        './sw-test/',
        './sw-test/index.html',
        './sw-test/style.css',
        './sw-test/app.js',
        './sw-test/image-list.js',
        …
        // include other new resources for the new version...
      ]);
    })
  );
});

跟开始缓存的时候,改变了下版本号v2,在这种情况下,以前的版本仍然负责获取。新版本将在后台安装。我们正在调用新的缓存v2,因此v1不会干扰以前的缓存。如果没有页面使用当前版本,则新工作程序将激活并负责提取。

Service Worker删除就缓存(activate)

因为存储量有限制,不可能让你无限的存储资源到客户端,不需要的缓存也应该需要抛弃。

self.addEventListener('activate', (event) => {
  var cacheKeeplist = ['v2'];

  event.waitUntil(
    caches.keys().then((keyList) => {
      return Promise.all(keyList.map((key) => {
        if (cacheKeeplist.indexOf(key) === -1) {
          return caches.delete(key);
        }
      }));
    })
  );
});

二、Memory Cache 内存缓存

image.png

说到内存缓存,需要了解下内存的生命周期,不管使用哪种编程语言,内存的生命周期几乎总是相同的:

1.分配你需要的内存
2.使用分配的内存(读、写)
3.当不再需要时释放已分配的内存

内存缓存主要包含访问页面中已经获取到的轻型资源,比如:js,css,html,pic等。访问速度是所有缓存里面最快的,但是持续性会随着进程结束而释放,也就是关闭了页面后,内存的缓存文件就会被释放,JavaScript利用一种称为垃圾收集(GC)的自动内存管理形式,垃圾回收器的作用是监视内存分配,并确定何时不再需要已分配内存块并回收它,所以,即便关闭页面,也未必完全释放内存,如果关闭访问过的页面打开控制台再次刷新看,发现很多数据来自内存缓存,就证明了内存没完全释放。
关于内存缓存,太深奥,有兴趣的可以查看相关文档,在此不做赘述。

三、Disk Catche 硬盘缓存

image.png

Disk Catche也叫HTTP Cahce,因为其严格遵守http响应头字段来判断哪些资源是否要被缓存,哪些资源是否已经过期,如同其名字,就是存储在本地硬盘上某块空间的资源,可以根据http的header头设置什么资源缓存,什么资源过期后重新请求。绝大多数缓存都是disk cache。

Disk Catche分为强制缓存对比缓存

1.强制缓存

不向服务器发送请求而从缓存中读取资源,通过控制台可以看到请求返回200,但是size显示 from memory cache和disk cache,强制缓存通过设置http header进行实现,
控制强制缓存的有两种方式:
一种为设置失效时间:Expires;
一种为cache-control;

1.1 Expires

来看下失效时间的响应头:
image.png
上图就是强制缓存请求的文件,访问请求返回200,从本地硬盘读取,当前时间是2021.08.03,设置的失效时间为2021.08.16(分钟不写了),也就是在8.16前访问,永远都是从disk catche返回给浏览器,不会从服务器重新获取,速度会比网络更快一些。
(注意expires上下两个key【etag和last-modified】)。
Expires是Web服务器响应消息头字段,响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。

1.2 cache-control

前端在head标签内的meta标签使用,Cache-Control 也在请求头或者响应头中设置。当Cache-Control:max-age=30时,则代表在这个请求正确返回时间的30秒内再次加载资源,就会命中强制缓存,这个控制目空一切,一切缓存给它让路。
比如:

// 设置缓存时间为1年
Cache-Control: max-age=31536000
在请求中使用Cache-Control 时,它可选的值有:
字段名称字段说明
no-cache告知(代堙)服务器不直接使用缓存,要求向原服务器发起请求
no-store所有内容都不会被保存到缓存或 Internet临时文件中
max-age=delta-seconds告知服务器客户端希望接收一个存在时间(Age)不大于delta-seconds秒的资源
max-stale[= delta-seconds]告知(代理)服务器客户端愿意接收一个超过缓存时间的资源,若有定义 delta-seconds则为 delta-seconds秒,若没有则为任意超出的时间
min-fresh=delta-seconds告知(代理)服务器客户端希望接收一个在小于delta-seconds秒内被更新过的资源
no-transform告知(代理服务器客户端希望获取实体数据没有被转换(比如压缩)过的资源
only-if-cached告知(代理)服务器客户端希望获取缓存的内容(若有),而不用向原服务器发去请求
cache-extension自定义扩展值,若服务器不识别该值将被忽略掉
在响应中使用Cache-Control 时,它可选的值有:
字段名称字段说明
public表明任何情况下都得缓存该资源(即使是需要HTTP认证的资源),(例如:1.该响应没有max-age指令或Expires消息头;2. 该响应对应的请求方法是 POST )
Private [="field-name]表明返回报文中全部或部分(若指定了 field-name则为fied4name的字殷数据)仅开放给某些用户(服务器指定的 chare-user,如代理服务器)做缓存使用,其他用户则不能缓存这些数据
no-cache不使用任何缓存,要求向服务器发起(新鲜度校验)请求
no-store所有内容都不会被保存到缓存或 Internet临时文件中
no-transform告知客户端缓存文件时不得对实体数据做任何改变
only-if-cached告知(代理)服务器客户端希望获取缓存的内容(若有)而不用向原服务器发去请求
must-revalidate当前资源一定是向原服务器发去验证请求的,若请求失败会 返回504(而非代理服务器上的缓存)
proxy-revalldate与 must-revalidate类似,但仅能应用于共享缓存(如代理)
max-age=delta-seconds告知客户端该资源在 delta-seconds秒内是新鲜的,无需向服务器发请求
s-maxage=delta-seconds同 max-age,但仅应用于共享缓存(如代理)
cache-extension自定义扩展值,若服务器不识别该值将被忽略掉

兼容性:
image.png

注意:
不要使用POST。大多数缓存并不保留对POST方法的响应。如果您在路径或查询中(通过GET)发送信息,则缓存可以将来存储该信息。
不管是max-age=0还是no-cache,都会返回304(资源无修改的情况下),no-store才是真正的不进行缓存。

Expires VS cache-control

两者差别不大, Expires是http1.0的产物,Cache-Control是http1.1的产物,两者同时存在则Cache-Control优先于Expires;在某些不支持HTTP1.1的环境下,Expires就会发挥用处。所以Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法。

强制缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容是否已经发生了更新呢?接下来需要涉及到协商缓存策略。

2.协商缓存(对比缓存)

协商缓存策略也就是对比缓存,通过对比进行缓存读取处理,协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:

1.协商缓存生效:返回304和Not Modified
2.协商缓存失效:返回200和请求结果

这两种情况的缓存标识:Modified 和 ETag
协商缓存结果如下图:

304情况:
image.png
200情况:
image.png

刚刚说过了,强制缓存是浏览器直接根据响应头Cache-Control字段直接判断缓存资源是否有效,有效就返回,就到不了协商缓存,因为优先级问题。举个例子,如果缓存时间过期了,资源也没有变化,难道重新请求新资源?肯定不是的,所以这个时候就展现协商缓存的时候了。协商缓存策略是需要再次向服务器确认资源是否有变化。怎么判断呢?通过Last-Modified & If-Modified-Since来评判。
浏览器在第一次访问资源时,服务器返回资源的同时,在response header中添加Last-Modified,值是这个资源在服务器上的最后修改时间,浏览器接收后缓存文件和header,如下图:
image.png
这时候该文件会缓存到本地,刷新后在访问如图:
image.png

为什么呢?浏览器再一次请求这个资源,浏览器检测到有Last-Modified这个header,于是添加If-Modified-Since这个header,值就是Last-Modified中的值;服务器再次收到这个资源请求,会根据If-Modified-Since中的值与服务器中这个资源的最后修改时间对比,如果没有变化,则返回304和空的响应体(服务器只会给你返回一个头信息,让你继续用你那过期的缓存,这样就节省了很多传输文件的时间带宽资源),直接从缓存读取,如果If-Modified-Since的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和200。
image.png

总结为:
image.png
Last-Modiflied与Expires一样,也是有缺陷的。如果资源的变化的时间间隔小于秒级,比如说是毫秒级的,或者说资源直接是动态生成的,那根据Last-Modified判断,资源就是每时每刻都最新的,即被修改过!如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源。
既然根据文件修改时间来决定是否缓存尚有不足,能否可以直接根据文件内容是否修改来决定缓存策略?所以在 HTTP/1.1HTTP1.1规范出现了ETag和If-None-Match,也是协商的一种。

ETag和If-None-Match

Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,Etag就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到request header里的If-None-Match里,服务器只需要比较客户端传来的If-None-Match跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现ETag匹配不上,那么直接以常规GET 200回包形式将新的资源(当然也包括了新的ETag)发给客户端;如果ETag是一致的,则直接返回304知会客户端直接使用本地缓存即可。
image.png

1、精确度上,Etag要优于Last-Modified。
Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。

2、性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。

3、优先级上,服务器校验优先考虑Etag。

Cache-Control/Expires:如果检测到本地的缓存还是有效的时间范围内,浏览器直接使用本地副本,不会发送任何请求。跟协商缓存一起使用时,Cache-Control/Expires的优先级要高于Last-Modified/ETag。即当本地副本根据Cache-Control/Expires发现还在有效期内时,则不会再次发送请求去服务器询问修改时间(Last-Modified)或实体标识(Etag)了。

一般情况下,使用Cache-Control/Expires会配合Last-Modified/ETag一起使用,因为即使服务器设置缓存时间, 当用户点击“刷新”按钮时,浏览器会忽略缓存继续向服务器发送请求,这时Last-Modified/ETag将能够很好利用304,从而减少响应开销。

key描述存储策略过期策略协商策略
Cache-Control指定缓存机制,覆盖其他设置
Pragmahttp1.0字段,指定缓存机制
Expireshttp1.0字段,指定缓存的过期时间
Last-Modified资源最后一次修改时间
ETag唯一标识请求资源的字符串

四、Push Cache 推送缓存

推送缓存是http2的内容,当上述三种缓存都没有命中就会被使用,只在session中存在,一旦结束就被释放,如同Memory Catch,在chrome中仅存在几分钟。所有的资源都能被推送,并且能够被缓存;可以推送 no-cache 和 no-store 的资源;一旦连接被关闭,Push Cache 就被释放;多个页面可以使用同一个HTTP/2的连接,也就可以使用同一个Push Cache;Push Cache 中的缓存只能被使用一次;浏览器可以拒绝接受已经存在的资源推送;也可以给其他域名推送资源。

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

缓存的应用场景

了解了浏览器缓存处理机制,那如何在真实场景中使用缓存策略来提高交互体验呢?

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

对于频繁变动的资源,首先需要使用Cache-Control: no-cache 使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。

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

通常在处理这类资源时,给它们的 Cache-Control 配置一个很大的 max-age=31536000 (一年),这样浏览器之后请求相同的 URL 会命中强制缓存。而为了解决更新的问题,就需要在文件名(或者路径)中添加 hash, 版本号等动态字符,之后更改动态字符,从而达到更改引用 URL 的目的,让之前的强制缓存失效 (其实并未立即失效,只是不再使用了而已)。
在线提供的类库 (如 jquery, 公共css,公共pic等) 均采用这个模式。

用户在浏览器操作时会触发怎样的缓存策略。主要有以下几种:
操作行为Expires/Cache-ControlLast-Modified/Etag
输入url,回车
页面链接跳转
新打开窗口
前进后退
刷新X
强制刷新XX
打开网页,地址栏输入地址后,资源请求查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。

普通刷新 (F5): TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache。

强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache(为了兼容,还带了 Pragma: no-cache),服务器直接返回 200 和最新内容。

终结:

浏览器缓存里, Cache-Control是金字塔顶尖的规则, 它藐视一切其他设置, 只要其他设置与其抵触, 一律覆盖之.不仅如此, 它还是一个复合规则, 包含多种值, 横跨 存储策略, 过期策略 两种, 同时在请求头和响应头都可设置。
Expires过期时间是其次,毕竟有王者在。
Last-Modified是其次次,最终是Etag。
合理利用缓存,逼格会提升。


mydetails
189 声望6 粉丝

一个技术