浏览器缓存是性能优化中最直接,高效的优化方式,它可以显著减少因为网络传输而带来的损耗。

  • 对于数据请求来说,大致可以分为 请求 -> 处理 -> 响应这三个步骤,而浏览器缓存则主要在第一步和第三步做手脚,即请求发出时寻找合适的缓存,拿到新的响应数据时做新的缓存。
  • 缓存带给我们最直观的感受就是,每次加载页面,第二次之后加载总是比第一次加载的更快,这就是缓存的功劳,下面讲从缓存位置和缓存策略两个方面介绍浏览器相关缓存。

缓存位置

  • 浏览器的缓存位置大概可以分为四种:
    1. Service Worker
    2. Memory Cache
    3. Disk Cache
    4. Push Cache(HTTP 2)
  • 他们有各自的优先级,当发出请求时,浏览器会依次去寻找缓存,如果都没有命中,才会发出请求。

Service Worker

  • Service Worker是浏览器背后的独立线程,可以用来实现缓存功能,但是需要注意,如果要使用Service Worker,传输协议必须是HTTPS
  • Service Worker实现缓存大致三个步骤。
    1. 注册Service Worker。
    2. 监听install事件,并对需要的文件进行缓存。
    3. 拦截HTTPS请求,并根据请求内容去命中缓存,如果命中,则直接使用缓存,否则请求数据。
  • 下面是一串实例代码(源代码来自前端面试之道)
// index.js
if (navigator.serviceWorker) {
  navigator.serviceWorker
    .register('sw.js')
    .then(function(registration) {
      console.log('service worker 注册成功')
    })
    .catch(function(err) {
      console.log('servcie worker 注册失败')
    })
}

// sw.js
// 监听install事件,缓存文件
self.addEventListener('install', e => {
  e.waitUntil(
    caches.open('my-cache').then(function(cache) {
      return cache.addAll(['./index.html', './index.js'])
    })
  )
})

// 拦截请求,并根据请求去命中响应数据
self.addEventListener('fetch', e => {
  e.respondWith(
    caches.match(e.request).then(function(response) {
      if (response) {
        return response
      }
      console.log('fetch source')
    })
  )
})

Memory Cache -> 内存缓存

  • 由于是内存缓存,所以速度会比后续马上讲到的磁盘缓存速度快很多,但是内存空间非常有限,所以一般内存缓存的内容都比较小,且生命周期短,会随着进程的释放而释放(关闭页面)。
  • 内存缓存一般是css,js,svg,小图片文件等,打开浏览器的开发者工具,重新刷新网络页面,可以发现很多请求右侧都有memory cache的提示,说明这些请求就是命中内存缓存中的内容而直接返回的内容。

内存缓存

Disk Cache

  • 磁盘缓存虽然速度没有内存缓存快,但是容量大,基本什么内容都可以存在磁盘中,并且可以存储内容的生命周期长。
  • 磁盘缓存会根据HTTP Header中国呢的字段判断哪些内容需要缓存,哪些资源可以直接使用,哪些资源已经过期需要重新请求。需要注意的是,相同地址的资源一旦被磁盘缓存下来,就不会再次去请求数据,比如我直接更改博客图片链接所指向的图片内容(链接不变),重新刷新后会发现图片依然是原来的图片。

Push Cache(Http2)

  • push cache是HTTP/2的内容,当上面三个缓存都没有命中时,才会被使用。并且缓存的时间非常短暂,只在session中存在,一旦session结束就会被释放。由于接触的不多,这里不细讲。

缓存策略

缓存策略分为两种:强缓存和协商缓存。并且它们的缓存策略都是通过HTTP Header来实现的。需要注意的是,这两种缓存并不是互斥的,即强缓存和协商缓存在一次请求中是同时存在的,下面会细说。

强缓存

  • 强缓存通过设置两种HTTP Header来实现:ExpiresCache-Control。强缓存表示的是在缓存期间内不需要请求,响应码为200.

Expires

Expires: Wed, 22 Oct 2018 08:41:00 GMT
  • Expires是HTTP/1的响应头,它表示的是请求的资源会在上述的时间后过期。如果资源会再次过期,并且Expires是基于本地时间进行判断的,如果人为更改了本地时间,可能会造成缓存失效。

Cache-Control

cache-control: public, max-age=31536000, s-maxage=31536000, immutable
  • Cache-Control是HTTP/1.1的请求/响应头,并且它的优先级高于Expires,该资源表示资源是可以被客户端,代理服务器缓存的,且缓存时间为一年。
  • 上述可以看到,Cache-Control可以使用多种指令,从而达到多个目的,下面是一些常见指令的作用。

cache-control指令

协商缓存

  • 协商缓存一般发生在强缓存不能进行的时候,当浏览器需要命中缓存却发现缓存已经过期时,它不会立即删除缓存,而是抱着试一试的态度,带着缓存的唯一标识信息去询问后端资源有没有更改,如果没有更改,就直接返回304(Not Modified),并更新本地缓存有效期

  • 和强缓存类似,协商缓存也需要借助HTTP Header字段的帮助,它们分别是Last-ModifiedE-Tag

Last-Modified 和 If-Modified-Since -> HTTP/1

  • Last-Modified 表示本地文件最后修改日期,If-Modified-Since会把Last-Modified的值发送给服务器,询问服务器在该时间段后,资源是否发生了更改,如果有更新,则会正常请求资源(200),并更新本地缓存,如果没有,则直接返回缓存资源(304)。

ETag 和 If-None-Match

  • ETag类似于文件id,If-None-Match会把ETag的值发送给服务器,并询问对应资源是否有更改,如果没有变动就直接返回本地缓存,如果有变动则返回新的资源,并更新本地缓存。需要注意的是,ETag的优先级比Last-Modified高