Skip to content
On this page

缓存机制

#缓存机制

缓存是性能优化中简单高效的一种优化方式,可以显著减少网络传输带来的损耗

我们发起一个网络请求,主要包含三个步骤:发起请求后端处理请求响应

我们可以在第一和第三步中优化性能,比如说直接使用缓存不发起请求发起请求,如果跟前端一致,不需返回数据

缓存位置

缓存在浏览器中主要存在于:

  • Service Worker
  • Memory Cache
  • Disk Cache
  • Push Cache
  • 网络请求

有各自的优先级,当依次查找缓存都没有命中时,才会去请求网络

Service Worker

Service Worker是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。它与其他的缓存机制不同,我们可以自由控制缓存那些文件,如何匹配缓存,如何读取缓存,并且缓存是持续性

传输协议必须为HTTPS,因为涉及到请求的拦截,保障安全。

使用Service Worker实现缓存功能的步骤:

  • 先注册Service Worker
  • 监听install事件,就可以缓存需要的数据
  • 下一次用户访问的时候可以通过拦截请求的方式,查询是否有缓存,有的话直接读取缓存,否则才去请求数据。
html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    // 注册Service Worker
    if (navigator.serviceWorker) {
      navigator.serviceWorker.register('serviceWorker.js')
      .then((registration)=>{
        console.log('serviceWorker 注册成功')
      })
      .catch((registration)=>{
        console.log('serviceWorker 注册失败')
      })
    }
  </script>
</body>
</html>
js
// serviceWorker.js
// 监听install事件
self.addEventListener('install', (e) => {
  e.waitUntil(
	// 打开缓存,缓存需要的数据
    caches.open('my-cache').then((cache) => {
      return cache.addAll(['./index.html'])
    })
  )
})
// 拦截所有请求事件
self.addEventListener('fetch', (e) => {
  // 在 Cache 对象中查询第一个匹配URL请求。如果没有发现匹配项,该代码将转而从网络获取响应。
  e.respondWith(
    caches.match(e.request).then((response) => {
      // 如果存在缓存,直接返回
      if (response) {
        return response;
      }
      // 否则请求资源
      console.log('fetch source')
    })
  )
})

Memory Cache

内存中的缓存

  • 高效
  • 缓存持续性短,随着进程的释放而释放
  • 当我们刷新页面时,很多数据都是来自内存缓存
  • 对大文件来说,大概率不存储在内存中
  • 如果当前系统的内存使用率高,文件会优先存储进硬盘

Disk Cache

存储在硬盘的缓存

  • 读取速度慢
  • 可以存储任何文件
  • 容量大,存储时效性
  • 在所有浏览器缓存中,占比是最大的
  • 根据HTTP Header中的字段判断资源是否需要请求,资源是否过期
  • 即使是跨站点的情况下,相同地址的资源一旦被硬盘缓存,也不会再次去请求数据

Push Cache

HTTP/2中的内容,当以上三种缓存都没命中时,才会使用

让服务器能够在客户端请求一个资源的时候,预先下发另外一个资源。

  • 缓存时间短暂,只在session中才生效,会话结束就会释放
  • 所有资源都能被推送,EdgeSafari兼容性不好
  • 可以推送no-cacheno-store的资源
  • 一旦连接关闭,Push Cache就被释放
  • 多个页面库使用相同的HTTP/2连接,也就是说能使用相同的缓存
  • Push Cache中的缓存只能被使用一次
  • 浏览器可以拒绝接受已经存在的资源推送
  • 可以给其他域名推送资源

参考链接

网络请求

如果所有缓存都没有命中的话,那么只能发起请求来获取资源了。

缓存策略

缓存策略均通过设置HTTP Header来实现的

强(制)缓存

强缓存表示缓存期间不需要请求,状态码为200

Expires

HTTP/1的产物。

表示到某个 HTTP 日期后到期/过期,资源过期后则需要重新请求。

但受限于本地时间,如果修改了本地时间,可能会造成缓存失效。

Cache-control

出现在HTTP/1.1,优先级高于Expires。默认值为private

常用指令:

  • public/private:注意private表示只有响应的最终接收方可以缓存,可以防止公共缓存(如cdn)存储个人信息
  • max-age=<seconds>定义了一个相对于请求时间的有效期,单位为秒,在规定时间内,浏览器不需要重新回访服务器验证该文件
    • 如果超过时间,有新的文件供浏览器下载,服务器返回200,浏览器下载该文件,会删除旧文件为缓存,应用新的缓存报头
    • 如果文件没有更新,那么服务器会返回304,不需要下载文件,用新的报文来替换旧的缓存
  • no-store表示不会以任何方式缓存
  • no-cache表示每次都向服务端去验证资源的新鲜性,是一种兼顾文件新鲜度和获取缓存资源的方式
    • 如果有新资源,返回200和新的资源
    • 如果没有,返回304,重用缓存中的版本
  • must-revalidate需要和max-age关联使用,当资源过期时,会立即重新访问服务器,如果没新资源,返回304,重新计时,反之,返回200,缓存更新。
    • 有宽期限的no-cache
    • 适用于博客场景

协商缓存

如果缓存过期了,就需要发起请求验证资源是否更新,当资源没有发生改变,那么返回304,并且更新浏览器缓存有效期。

Last-Modified和If-Modified-Since

Last-Modified存在于response header中,If-Modified-Since存在于request header中。

Last-Modified表示本地文件最后修改日期,If-Modified-Since会将Last-Modified的值发送给服务器,询问该日期后是否有更新,有的话将新的资源发过来,没有的话返回304

但是存在一些弊端:

  • 在本地打开缓存文件,即使内容没修改,Last-Modified也会被修改,服务器不能命中缓存导致发送相同的资源
  • 以秒计时,如果在不可感知的时间内修改文件,那么服务端会以为资源还是命中了,不会返回正确的资源

ETag和If-None-Match

ETag 类似于文件指纹,If-None-Match 会将当前 ETag 发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。并且 ETag 优先级比 Last-Modified 高

如果什么缓存策略都没设置,那么浏览器会采用一个启发式的算法,通常会取响应头中的date减去Last-Modified值的10%作为缓存时间

实际场景应用

频繁变动的资源

使用no-cache使浏览器每次都请求服务器,配合ETag或者Last-Modified来验证资源是否有效。这种方法无法减少请求次数,但能显著减少响应数据大小。

代码文件

特指HTML以外的代码文件,HTML一般不缓存或者缓存时间很短。

在打包的时候可以对文件名进行Hash处理,只有代码被修改了,才会生成新的文件名。

基于此,我们可以给代码文件设置缓存有效期一年:Cache-Control:max-age=31536000,这样只有当HTML文件中引用的文件名发生改变时,才会去下载新的代码文件,否则就一直使用缓存。

MIT Licensed | Copyright © 2021 - 2022