阅读 109

浏览器缓存机制与缓存策略

这是我参与更文挑战的第7天,第二篇,活动详情查看: 更文挑战

缓存可以减少网络 IO 消耗,提高访问速度。浏览器缓存是一种操作简单、效果显著的前端性能优化手段。

对于这个操作的必要性,Chrome 官方给出的解释似乎更有说服力一些:

通过网络获取内容既速度缓慢又开销巨大。较大的响应需要在客户端与服务器之间进行多次往返通信,这会延迟浏览器获得和处理内容的时间,还会增加访问者的流量费用。因此,缓存并重复利用之前获取的资源的能力成为性能优化的一个关键方面。

缓存类型

很多时候,大家倾向于将浏览器缓存简单地理解为“HTTP 缓存”。但事实上,浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列如下:

  • Memory Cache
  • Service Worker Cache
  • HTTP Cache
  • Push Cache

大家对 HTTP Cache(即 Cache-Control、expires 等字段控制的缓存)应该比较熟悉,如果对其它几种缓存可能还没什么概念:形如“(from xxx)”这样的描述——对应的资源,这些资源就是我们通过缓存获取到的。

  • “from memory cache”对标到 Memory Cache 类型
  • “from ServiceWorker”对标到 Service Worker Cache 类型
  • Push Cache,这个比较特殊,是 HTTP2 的新特性。

HTTP 缓存机制

HTTP 缓存是我们日常开发中最为熟悉的一种缓存机制。它又分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存。

在之前的文章《http协议详解之一:缓存(熟悉的304)》中已经详细介绍了http缓存,这里不再赘述。

MemoryCache

MemoryCache,是指存在内存中的缓存。从优先级上来说,它是浏览器最先尝试去命中的一种缓存。从效率上来说,它是响应速度最快的一种缓存。

内存缓存是快的,也是“短命”的。它和渲染进程“生死相依”,当进程结束后,也就是 tab 关闭以后,内存里的数据也将不复存在。

那么哪些文件会被放入内存呢?

事实上,这个划分规则,一直以来是没有定论的。不过想想也可以理解,内存是有限的,很多时候需要先考虑即时呈现的内存余量,再根据具体的情况决定分配给内存和磁盘的资源量的比重——资源存放的位置具有一定的随机性。

虽然划分规则没有定论,但根据日常开发中观察的结果,我们至少可以总结出这样的规律:资源存不存内存,浏览器秉承的是“节约原则”。

  • Base64 格式的图片,几乎永远可以被塞进 memory cache,这可以视作浏览器为节省渲染开销的“自保行为”
  • 体积不大的 JS、CSS 文件,也有较大地被写入内存的几率——相比之下,较大的 JS、CSS 文件就没有这个待遇了,内存资源是有限的,它们往往被直接甩进磁盘。

Service Worker Cache

Service Worker 是一种独立于主线程之外的 Javascript 线程。它脱离于浏览器窗体,因此无法直接访问 DOM。这样独立的个性使得 Service Worker 的“个人行为”无法干扰页面的性能,这个“幕后工作者”可以帮我们实现离线缓存、消息推送和网络代理等功能。我们借助 Service worker 实现的离线缓存就称为 Service Worker Cache。

Service Worker 的生命周期包括 install、active、working 三个阶段。一旦 Service Worker 被 install,它将始终存在,只会在 active 与 working 之间切换,除非我们主动终止它。这是它可以用来实现离线存储的重要先决条件。

下面我们就通过实战的方式,一起见识一下 Service Worker 如何为我们实现离线缓存(注意看注释): 我们首先在入口文件中插入这样一段 JS 代码,用以判断和引入 Service Worker:

window.navigator.serviceWorker.register('/test.js').then(
   function () {
      console.log('注册成功')
    }).catch(err => {
      console.error("注册失败")
    })
复制代码

在 test.js 中,我们进行缓存的处理。假设我们需要缓存的文件分别是 test.html,test.css 和 test.js:

// Service Worker会监听 install事件,我们在其对应的回调里可以实现初始化的逻辑  
self.addEventListener('install', event => {
  event.waitUntil(
    // 考虑到缓存也需要更新,open内传入的参数为缓存的版本号
    caches.open('test-v1').then(cache => {
      return cache.addAll([
        // 此处传入指定的需缓存的文件名
        '/test.html',
        '/test.css',
        '/test.js'
      ])
    })
  )
})

// Service Worker会监听所有的网络请求,网络请求的产生触发的是fetch事件,我们可以在其对应的监听函数中实现对请求的拦截,进而判断是否有对应到该请求的缓存,实现从Service Worker中取到缓存的目的
self.addEventListener('fetch', event => {
  event.respondWith(
    // 尝试匹配该请求对应的缓存值
    caches.match(event.request).then(res => {
      // 如果匹配到了,调用Server Worker缓存
      if (res) {
        return res;
      }
      // 如果没匹配到,向服务端发起这个资源请求
      return fetch(event.request).then(response => {
        if (!response || response.status !== 200) {
          return response;
        }
        // 请求成功的话,将请求缓存起来。
        caches.open('test-v1').then(function(cache) {
          cache.put(event.request, response);
        });
        return response.clone();
      });
    })
  );
}
复制代码

大家注意 Server Worker 对协议是有要求的,必须以 https 协议为前提。

Push Cache

Push Cache 是指 HTTP2 在 server push 阶段存在的缓存。这块的知识比较新,应用也还处于萌芽阶段,我找了好几个网站也没找到一个合适的案例来给大家做具体的介绍。但应用范围有限不代表不重要——HTTP2 是趋势、是未来。在它还未被推而广之的此时此刻,我仍希望大家能对 Push Cache 的关键特性有所了解:

  • Push Cache 是缓存的最后一道防线。浏览器只有在 Memory Cache、HTTP Cache 和 Service Worker Cache 均未命中的情况下才会去询问 Push Cache。
  • Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。
  • 不同的页面只要共享了同一个 HTTP2 连接,那么它们就可以共享同一个 Push Cache。

小结

页面的数据存储方案除了缓存,还有本地存储。缓存部分的知识,具有“细碎、迭代快”的特点。

以上! 最后的惯例,贴上我的博客,欢迎关注

请关注公众号:全栈飞行中队

参考资料 HTTP/2 push is tougher than I thought

以上! 最后的惯例,贴上我的博客,欢迎关注

请关注公众号:全栈飞行中队

文章分类
前端
文章标签