浏览器缓存策略

本文github地址,欢迎star

浏览器缓存策略_第1张图片

缓存是提高数据读取读取性能的技术,在软件开发中广泛使用,比如常见的CPU缓存、数据库缓存、浏览器缓存等。

缓存Web前端性能优化的必要手段之一,既能保证用户在第一时间里获取到最新的资源,又能减少网络请求。浏览器缓存策略主要包括以下两种:

  • HTTP缓存
  • ServiceWorker

HTTP 缓存

HTTP缓存需要浏览器和服务器进行配置使用,比如NginxApache可以设置不同的HTTP缓存策略。
HTTP缓存主要是对浏览器的静态文件起作用,分为强制缓存协商缓存

强制缓存

强制缓存策略是浏览器根据资源过期时间来决定,是否使用缓存。如果在缓存中找到资源,没有过期,则直接使用缓存资源,状态码是200, 否则直接进行请求。

强制缓存是根据ExpiresCache-control来决定的。

Expires

HTTP/1.0 中可以使用响应头部字段 Expires 来设置缓存时间,它对应一个未来的时间戳。

expires: Thu, 06 Mar 2031 12:35:43 GMT

Expires有一个致命的缺点是: 它采用的是时间是以服务器的为准,但是浏览器进行判断是将本地的时间与此时间进行对比,这样会导致时间精度上存在误差。因此为了为了更精准的控制资源,http/1.1新增了Cache-control响应头字段。

Cache-control

Cache-control是新增了http/1.1响应头字段,用来控制浏览器强制缓存

它的常用值有下面几个:

字段 说明
no-cache 表示使用协商缓存,即每次使用缓存前必须向服务端确认缓存资源是否更新;
no-store 禁止浏览器以及所有中间缓存存储响应内容
public 公有缓存,表示可以被代理服务器缓存,可以被多个用户共享
private 私有缓存,不能被代理服务器缓存,不可以被多个用户共享
max-age 以秒为单位的数值,表示缓存的有效时间
must-revalidate 当缓存过期时,需要去服务端校验缓存的有效性

其值可以组合使用:

cache-control: public, max-age=31536000

需要注意的是Expirescache-controlmax-age 优先级高于 Expires,如果它们同时出现,浏览器会优先使用 max-age 的值。

浏览器缓存策略_第2张图片

协商缓存

协商缓存策略是每次请求都会发送请求,经过服务器端对资源的对比,来决定是使用本地缓存,还是新的资源。请求响应返回的 HTTP 状态为 304,则表示缓存仍然有效。控制缓存的难题就是从浏览器端转移到了服务端。

控制协商缓存有两种方式:

  • Last-ModifiedIf-Modified-Since
  • ETagIf-None-Match

Last-Modified 和 If-Modified-Since

服务端要判断缓存有没有过期,采取的方式是资源进行对比。如果浏览器直接把资源发送给服务端进行比对的话,网络开销太大,而且也会失去缓存的意义,所以显然是不可取的。

有一种简单的判断方法,是通过响应头部字段 Last-Modified 和请求头部字段 If-Modified-Since 比对双方资源的修改时间。

工作流程如下:

  1. 浏览器第一次请求资源,服务端在返回资源的响应头中加入 Last-Modified 字段,该字段表示这个资源在服务端上的最近修改时间。
  2. 当浏览器再次向服务端请求该资源时,请求头部带上之前服务端返回的修改时间,这个请求头叫 If-Modified-Since
  3. 服务端再次收到请求,根据请求头 If-Modified-Since 的值,判断相关资源是否有变化,如果没有,则返回 304 Not Modified,并且不返回资源内容,浏览器使用资源缓存值;否则正常返回资源内容,且更新 Last-Modified 响应头内容。

这种方式虽然能判断缓存是否失效,但也存在两个问题:

  • 精度问题,Last-Modified 的时间精度为秒,如果在 1 秒内发生修改,那么缓存判断可能会失效。
  • 准度问题,如果一个文件被修改,然后又被还原,内容并没有发生变化,在这种情况下,浏览器的缓存还可以继续使用,但因为修改时间发生变化,也会重新返回重复的内容。

ETag 和 If-None-Match

为了解决精度问题和准度问题,HTTP 提供了另一种不依赖于修改时间,而依赖于文件哈希值的精确判断缓存的方式,那就是响应头部字段 ETag 和请求头部字段 If-None-Match

工作流程如下:

  1. 浏览器第一次请求资源,服务端会在响应头中加入 Etag 字段,Etag 字段值为该资源的哈希值;
  2. 浏览器再次从服务端请求这个资源时,会在请求头上加上 If-None-Match,值为之前响应头部字段 ETag 的值;
  3. 服务端再次收到请求,将请求头 If-None-Match 字段的值和响应资源的哈希值进行比对,如果两个值相同,则说明资源没有变化,返回 304 Not Modified;否则就正常返回资源内容,无论是否发生变化,都会将计算出的哈希值放入响应头部的ETag 字段中。

这种缓存比较的方式也会存在一些问题,具体表现在以下两个方面。

  • 计算成本。生成哈希值相对于读取文件修改时间而言是一个开销比较大的操作,尤其是对于大文件而言。如果要精确计算则需读取完整的文件内容,如果从性能方面考虑,只读取文件部分内容,又容易判断出错。
  • 计算误差。HTTP 并没有规定哈希值的计算方法,不同服务端可能会采用不同的哈希值计算方式。这样带来的问题是,同一个资源,在两台服务端产生的 Etag 可能是不相同的,所以对于使用服务器集群来处理请求的网站来说,使用 Etag 的缓存命中率会有所降低。

需要注意:协商缓存中,Etag 优先级比 Last-Modified 高。

浏览器缓存策略_第3张图片

ServiceWorker

Service workers 可以理解为充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。主要目的是实现离线缓存,它会拦截网络请求并根据网络是否可用采取来适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API

使用方法

可以分为三步:

  1. 注册
  2. 安装
  3. 监听

在使用 ServiceWorker 脚本之前先要通过“注册”的方式加载它。

if ('serviceWorker' in window.navigator) {
  window.navigator.serviceWorker
    .register('./sw.js')
    .then(console.log)
    .catch(console.error)
} else {
  console.warn('浏览器不支持 ServiceWorker!')
}

考虑到浏览器的兼容性,判断 window.navigator 中是否存在 serviceWorker 属性,然后通过调用这个属性的 register 函数来告诉浏览器 ServiceWorker 脚本的路径。

浏览器获取到 ServiceWorker 脚本之后会进行解析,解析完成会进行安装。可以通过监听 “install” 事件来监听安装,但这个事件只会在第一次加载脚本的时候触发。要让脚本能够监听浏览器的网络请求,还需要激活脚本。

在脚本被激活之后,我们就可以通过监听 fetch 事件来拦截请求并加载缓存的资源了。

下面是上面”注册“的sw.js文件的内容:

const CACHE_NAME = 'ws'
// 这里是为什么是数组呢?因为们可以监听多个资源
let preloadUrls = ['/index.css']

/* 监听安装事件,install 事件一般是被用来设置你的浏览器的离线缓存逻辑 */
self.addEventListener('install', function (event) {
  /* 通过这个方法可以防止缓存未完成,就关闭serviceWorker */
  event.waitUntil(
     /* 创建一个名叫 CACHE_NAME 的缓存版本 */
    caches.open(CACHE_NAME)
    .then(function (cache) {
       /* 指定要缓存的内容,地址为相对于跟域名的访问路径 */
      return cache.addAll(preloadUrls);
    })
  );
});
/* 注册 fetch 事件,拦截全站的请求 */
self.addEventListener('fetch', function (event) {
  event.respondWith(
    /* 在缓存中匹配对应请求资源直接返回 */
    caches.match(event.request)
    .then(function (response) {
      // 匹配上直接返回缓存资源
      if (response) {
        return response;
      }
      return caches.open(CACHE_NAME).then(function (cache) {
          const path = event.request.url.replace(self.location.origin, '')
          return cache.add(path)
        })
        .catch(e => console.error(e))
    })
  );
})

这段代码首先监听 install 事件,在回调函数中调用了 event.waitUntil() 函数并传入了一个 Promise 对象。event.waitUntil 用来监听多个异步操作,包括缓存打开和添加缓存路径。如果其中一个操作失败,则整个 ServiceWorker 启动失败。

然后监听了 fetch 事件,在回调函数内部调用了函数 event.respondWith() 并传入了一个 Promise 对象,当捕获到 fetch 请求时,会直接返回 event.respondWith 函数中 Promise 对象的结果。

在这个 Promise 对象中,我们通过 caches.match 来和当前请求对象进行匹配,如果匹配上则直接返回匹配的缓存结果,否则返回该请求结果并缓存。

使用限制

    • ServiceWorker 中无法直接访问 DOM,但可以通过 postMessage 接口发送的消息来与其控制的页面进行通信;
    • ServiceWorker 只能在本地环境下或 HTTPS 网站中使用;
    • ServiceWorker 有作用域的限制,一个 ServiceWorker 脚本只能作用于当前路径及其子路径;
    • 由于 ServiceWorker 属于实验性功能,所以兼容性方面会存在一些问题。

    总结

    • Expirescache-controlmax-age 优先级高于 Expires
    • 强缓存的优先级高于协商缓存
    • 协商缓存中,Etag 优先级比 Last-Modified 高。
    • ServiceWorker 可以用来实现离线缓存,主要实现原理是拦截浏览器请求并返回缓存的资源文件。

    你可能感兴趣的:(浏览器缓存策略)