浏览器缓存机制

缓存

《图解 HTTP》一书中说:缓存是指代理服务器或者客户端本地磁盘内保存的资源副本,利用缓存可减少对服务器的访问。

说的稍微通俗一点,浏览器缓存就是把一个已经请求过的 Web 资源拷贝一份副本储存在浏览器中,缓存会根据进来的请求保存输出内容的副本,当下一个请求来到的时候,如果是相同的 URL,缓存会根据缓存机制决定是直接使用副本响应访问请求,还是向源服务器再次发送请求。

缓存的优点:

  • 节省通信流量。
  • 减少通信时间。
  • 降低服务器压力。

浏览器第一次向服务器发起该请求后拿到请求结果后,将请求结果和缓存标识存入浏览器缓存,浏览器对于缓存的处理是根据第一次请求资源时返回的响应头来确定的,浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识,浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中

缓存分类

缓存也是有好几种的,从缓存的位置上来说分为四种:

  • Service Worker
  • Memory Cache
  • Disk Cache
  • Push Cache

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

Service Worker

它是运行在浏览器背后的独立线程,使用它的前提是网络传输协议必须是 HTTPS,原因是 Service Worker 中涉及到请求拦截,所以使用 HTTPS 可以保障安全。

Service Worker 实现缓存机制需要先注册 Service Worker,监听到 Service Worker 状态为 installing 后就可以缓存需要的文件了。

if ('serviceWorker' in navigator) {
  // 验证浏览器是否支持Service Worker
  navigator.serviceWorker
    .register('./sw-demo-cache.js', {
      // 注册Service Worker
      scope: './',
    })
    .then(function (registration) {
      let serviceWorker
      if (registration.installing) {
        // 监听Service Worker状态
        serviceWorker = registration.installing
      }
    })
}

Service Worker 的状态(生命周期)如下:

installing --> installed --> activating --> activated

Memory Cache

它是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等,内存中的缓存读取速度比硬盘缓存快得多,但是内存中的缓存属于临时缓存,当我们关闭页面,缓存自然就没了。

内存缓存中的 preloader 是页面优化的常见手段之一。

preloader可以实现响应式加载资源,如:

<link rel="preload" as="image" href="map.png" media="(max-width: 600px)" />
<link rel="preload" as="script" href="map.js" media="(min-width: 601px)" />

它可以一边解析 js/css 文件,一边网络请求下一个资源。

需要注意的事情是,内存缓存在缓存资源时并不关心返回资源的 HTTP 缓存头 Cache-Control 是什么值,同时资源的匹配也并非仅仅是对 URL 做匹配,还可能会对 Content-TypeCORS 等其他特征做校验。

Disk Cache

Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,且储存时间比内存缓存长的多。

它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据,绝大部分缓存都来自硬盘缓存。

浏览器会根据情况选择将文件存储在内存还是硬盘中,对于大文件来说,大概率是不存储在内存中的,反之优先,当前系统内存使用率高的话,文件优先存储进硬盘。

Push Cache

Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在 Chrome 浏览器中只有 5 分钟左右,同时它也并非严格执行 HTTP 头中的缓存指令。

缓存过程

HTTP 请求将缓存过程分为两个部分,分别是强缓存和协商缓存。

强缓存

不会向服务器发送请求,直接从缓存中读取资源,在 chrome 控制台的 Network 选项中可以看到该请求返回 200 的状态码,并且 Size 显示 from disk cachefrom memory cache,强缓存可以通过设置两种 HTTP Header 实现:ExpiresPragmaCache-Control

浏览器收到资源时,会根据 Response Header 来判断是否对资源进行缓存,如果响应头中 ExpiresPragma 或者 Cache-Control 字段,代表这是强缓存,浏览器就会把资源缓存在 Memory cache 或 Disk cache 中。

对于这些缓存资源,浏览器会按照如下顺序查找:

Service Worker --> Memory Cache --> Disk Cache --> Push Cache

Expires

ExpiresHTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效,Expires: Wed, 22 Oct 2018 08:41:00 GMT 表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。

document.cookie = `clr=red; expires=${expiresDate}`

Pragma

这个是 HTTP1.0 中禁用网页缓存的字段,其取值为 no-cache,和 Cache-Controlno-cache 效果一样。

Cache-Control

HTTP/1.1 中,Cache-Control 是最重要的规则,主要用于控制网页缓存。比如当 Cache-Control:max-age=300 时,则代表在这个请求正确返回时间(浏览器也会记录下来)的 5 分钟内再次加载资源,就会命中强缓存。

Cache-Control 常见的取值有 publicprivateno-cachemax-age 等,默认为 private

  • public:所有内容都将被缓存(客户端和代理服务器都可缓存)。
  • private:所有内容只有客户端可以缓存,而不允许共享高速缓存(如 CDN)去缓存。
  • no-cache:客户端缓存内容,是否使用缓存则需要经过协商缓存来验证决定。
  • max-agemax-age=30 表示缓存内容将在 30 秒后失效。
  • no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存。
  • s-maxage(单位为 s):同 max-age 作用一样,只在代理服务器中生效(比如 CDN 缓存)。
  • max-stalemax-stale=30 30 秒内,即使缓存过期,也使用该缓存。
  • min-freshmin-fresh=30 希望在 30 秒内获取最新的响应。

两者同时存在的话,Cache-Control 优先级高于 Expires

max-age 与 s-maxage

在现实世界中,为了加快网站响应速度,我们可能会在浏览器和服务器之间引入 CDN 服务。浏览器的请求会先到达 CDN,然后 CDN 判断是从缓存中读取数据还是回源到服务器。CDN 也会识别源站响应头中 Cache-Control 属性,根据 max-age 设置的时间进行缓存,但是,如果源站同时设置了 s-maxagemax-age,那么 CDN 会优先采用 s-maxage

理论上,资源一旦发生了变更,那么可以通过更改文件名来更新资源,这叫做 cache busting。但实际使用中诸如 index.html 无法更改文件名。如果这一类文件较为频繁的更新,我们可能希望用户浏览器访问的时候总能拿到最新的资源。但又不希望 CDN 缓存击穿,所以可以用上 s-maxage 这个参数。

浏览器通常会看 cache-control: max-age=xxx 这个参数,决定在某一段时间内本地缓存是新鲜的,不会向服务器发起请求。

CDN 通常也会遵循这个头,如果仅仅设置 cache-control: max-age=0,固然每次浏览器会向 CDN 请求验证资源新鲜度,但是也会造成 CDN 每次都回源验证,会引起缓存击穿的问题。

因此可以参考:

cache-control: max-age=0,s-maxage=604800

这样的话,每次浏览器检测到 max-age=0 就会向 CDN 服务器验证资源是否新鲜,CDN 服务器检测s-maxage属性,计算是否达到了过期时间,过期了才会向服务器请求。

协商缓存

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程。

协商缓存可以通过设置两种 HTTP Header 实现:Last-ModifiedETag

Last-Modified

Last-Modifiedhttp1.0 的特性,浏览器在第一次访问资源时,服务器返回资源的同时,在 response header 中添加 Last-Modifiedheader,值是这个资源在服务器上的最后修改时间,浏览器接收后缓存文件和 header

Last-Modified: Fri, 22 Jul 2016 01:47:00 GMT

注意:

  • 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源。
  • 因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源。

ETag

ETaghttp1.1 的特性,ETag 是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,ETag 就会重新生成。

在精确度上,ETag 要优于 Last-Modified

在性能上,ETag 要逊于 Last-Modified

在优先级上,服务器校验优先考虑 ETag

为什么要有 ETag?

有的文件或者数据可能是动态生成的,但是,文件内容可能并没有变,这个时间是变了的,那么就会造成一定会更新,那么设置这个 Last-Modified 就失效了。使用 ETag 就更可靠一些。

缓存机制

强制缓存优先于协商缓存进行,若强制缓存(ExpiresCache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-SinceETag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回 200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回 304,继续使用缓存。

启发式缓存

如果响应中未显示 ExpiresCache-Control:max-ageCache-Control:s-maxage,并且响应中不包含其他有关缓存的限制,缓存可以使用启发式方法计算新鲜度寿命。通常会根据响应头中的 2 个时间字段 Date 减去 Last-Modified 值的 10% 作为缓存时间。

用户行为对浏览器缓存的影响

  • 打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。
  • 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache,浏览器会在请求头加上 If-Modify-since 判断缓存文件是否过期。
  • 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache(为了兼容,还带了 Pragma: no-cache),服务器直接返回 200 和最新内容。

缓存代理

代理转发响应时,缓存代理(Caching Proxy)会预先将资源的副本(缓存)保存在代理服务器上。当代理再次接收到对相同资源的请求时,就可以不从源服务器那里获取资源,而是将之前缓存的资源作为响应返回。

缓存是指代理服务器或客户端本地磁盘内保存的资源副本。利用缓存可减少对源服务器的访问,因此也就节省了通信流量和通信时间。

缓存服务器是代理服务器的一种,并归类在缓存代理类型中。换句话说,当代理转发从服务器返回的响应时,代理服务器将会保存一份资源的副本。

nginx 就是一种常见的代理服务器,nginx 使用 proxy_cache 将用户的请求缓存到本地一个目录。下一个相同请求可以直接调取缓存文件,就不用去请求服务器了。

Vary

Vary 主要用在缓存,用来告诉缓存代理此报文依据的是哪些请求头字段返回。

机制如下:

  1. 返回带有 Vary 字段的响应报文给代理服务器,然后代理服务器将【w/ URL and hash】作为 key,该响应报文作为 value 存入缓存中。
  • hash 是根据 vary 字段的值,在该报文中提取出相应的值计算 hash 所得。
  1. 当有新的请求访问代理服务器时,通过计算请求报文的相关头字段,得到相应的 key,看缓存中是否存在。
    • 存在,则返回缓存。
    • 否则,向源服务器获取最新资源了。

参考文章