# 缓存机制

缓存是性能优化中非常重要的一环,浏览器的缓存机制对开发也是非常重要的知识点。

浏览器缓存,也称Http缓存,分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存。

接下来以三个部分来把浏览器的缓存机制说清楚:

  • 强缓存
  • 协商缓存
  • 缓存位置

# 强缓存

浏览器中的缓存作用分为两种情况,一种是需要发送HTTP请求,一种是不需要发送。

首先是向浏览器缓存发起检查,这个阶段不需要发送HTTP请求。

# Expires

Expires即过期时间,存在于服务端返回的响应头中,告诉浏览器在这个过期时间之前可以直接从缓存里面获取数据,无需再次请求。比如下面这样:

Expires: Wed, 22 Nov 2019 08:41:00 GMT

表示资源在2019年11月22号8点41分过期,过期了就得向服务端发请求。

这个方式看上去没什么问题,合情合理,但其实潜藏了一个坑,那就是服务器的时间和浏览器的时间可能并不一致,那服务器返回的这个过期时间可能就是不准确的。因此这种方式很快在后来的HTTP1.1版本中被抛弃了。

# Cache-Control

在HTTP1.1 新增了 Cache-Control 字段来完成 expires 的任务。

它和Expires本质的不同在于它并没有采用具体的过期时间点这个方式,而是采用过期时长来控制缓存,对应的字段是max-age。比如这个例子:

Cache-Control: max-age=3600

代表这个响应返回后在 3600 秒,也就是一个小时之内可以直接使用缓存。

Cache-Control可以在请求头或者响应头中设置,并且可以组合使用多种指令:

# 总结

Expires 是http1.0的产物,Cache-Control 是http1.1的产物。

Cache-Control 相对于 Expires 更加准确,它的优先级也更高。

两者同时存在的话,Cache-Control优先级高于Expires;在某些不支持HTTP1.1的环境下,Expires就会发挥用处。所以Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法。

  • 缓存生效:返回 200 OK (from memory cache) (from disk cache)
  • 缓存失效:进入协商缓存

# 协商缓存

协商缓存依赖于服务端与浏览器之间的通信。

浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:

  • 协商缓存生效,返回304和Not Modified
  • 协商缓存失效,返回200和请求结果

协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag 。

# Last-Modified

即最后修改时间。在浏览器第一次给服务器发送请求后,服务器会在响应头(Response Headers)中加上这个字段:

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

浏览器接收到后,如果再次请求,会在请求头中携带If-Modified-Since的字段,它的值正是上一次 response 返回给它的 last-modified 值:

If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT

服务器拿到请求头中的If-Modified-Since的字段后,会跟服务器中该资源的最后修改时间进行对比:

  • 如果发生了变化,就会返回新的资源和200,跟常规的HTTP请求响应的流程一样。并在 Response Headers 中添加新的 Last-Modified 值;
  • 否则返回304,告诉浏览器直接从缓存读取。Response Headers 不会再添加 Last-Modified 字段。

# ETag

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

浏览器接收到ETag的值,会在下次请求时,将这个值作为If-None-Match这个字段的内容,并放到请求头中,然后发给服务器。

服务器接收到If-None-Match后,会跟服务器上该资源的ETag进行比对:

  • 如果两者不一样,说明要更新了。返回新的资源和200,跟常规的HTTP请求响应的流程一样。
  • 否则返回304,告诉浏览器直接用缓存。

# 两者对比

  1. 在精准度上,ETag优于Last-Modified

由于ETag是按照内容给资源上标识,因此能准确感知资源的变化。而Last-Modified就不一样了,它在一些特殊的情况并不能准确感知资源变化,主要有两种情况:

  • 编辑了资源文件,但是文件内容并没有更改,这样也会造成缓存失效。
  • Last-Modified能够感知的单位时间是秒,如果文件在 1 秒内改变了多次,那么这时候的Last-Modified并没有体现出修改了。
  1. 在性能上,Last-Modified优于ETag,也很简单理解,Last-Modified仅仅只是记录一个时间点,而Etag需要根据文件的具体内容生成哈希值。

  2. 在优先级上,服务器校验优先考虑Etag

# 缓存机制

强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match)

协商缓存由服务器决定是否使用缓存,

若协商缓存失效,那么代表该请求的缓存失效,返回200,重新返回资源和缓存标识,再存入浏览器缓存中;

生效则返回304,继续使用缓存。

# 缓存位置

前面我们已经提到,当强缓存命中或者协商缓存中服务器返回304的时候,我们直接从缓存中获取资源。那这些资源究竟缓存在什么位置呢?

浏览器中的缓存位置一共有四种,按优先级从高到低排列分别是:

  • Service Worker
  • Memory Cache(内存缓存)
  • Disk Cache(硬盘缓存)
  • Push Cache(推送缓存)

# Service Worker

和Web Worker类似,是独立的线程,我们可以在这个线程中缓存文件,在主线程需要的时候读取这里的文件,Service Worker使我们可以自由选择缓存哪些文件以及文件的匹配、读取规则,并且缓存是持续性的

# Memory Cache 和 Disk Cache

Memory Cache指的是内存缓存,从效率上讲它是最快的。但是从存活时间来讲又是最短的,当渲染进程结束后,内存缓存也就不存在了。

Disk Cache就是存储在磁盘中的缓存,从存取效率上讲是比内存缓存慢的,但是他的优势在于存储容量和存储时长。稍微有些计算机基础的应该很好理解,就不展开了。

好,现在问题来了,既然两者各有优劣,那浏览器如何决定将资源放进内存还是硬盘呢?主要策略如下:

  • 比较大的JS、CSS文件会直接被丢进磁盘,反之丢进内存
  • 内存使用率比较高的时候,文件优先进入磁盘

# Push Cache

即推送缓存,这是浏览器缓存的最后一道防线。是HTTP/2的内容,目前应用较少

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。

# 缓存策略

# 频繁变动的资源

Cache-Control: no-cache

对于频繁变动的资源,首先需要使用 Cache-Control: no-cache 使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。

# 不常变化的资源

Cache-Control: max-age=31536000

通常在处理这类资源时,给它们的 Cache-Control 配置一个很大的 max-age=31536000 (一年),这样浏览器之后请求相同的 URL 会命中强制缓存。而为了解决更新的问题,就需要在文件名(或者路径)中添加 hash, 版本号等动态字符,之后更改动态字符,从而达到更改引用 URL 的目的,让之前的强制缓存失效 (其实并未立即失效,只是不再使用了而已)。

在线提供的类库 (如 jquery-3.3.1.min.js, lodash.min.js 等) 均采用这个模式。

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

所谓用户行为对浏览器缓存的影响,指的就是用户在浏览器如何操作时,会触发怎样的缓存策略。主要有 3 种:

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

# 总结

对浏览器的缓存机制来做个简要的总结:

首先通过Cache-Control验证强缓存是否可用

  • 向浏览器缓存发起请求,如果强缓存可用,直接使用
  • 否则进入协商缓存,即发送HTTP请求,服务器通过请求头中的If-Modified-Since或者If-None-Match这些条件请求字段检查资源是否更新
    • 若资源更新,返回资源和200状态码
    • 否则,返回304,告诉浏览器直接从缓存获取资源,再从浏览器缓存获取资源