前言

  • 部署代码后,你跟测试大佬说刷新下页面就行了。为什么要刷新页面呢?刷新一次行嘛?还是要强制刷新?新打开一个tab行不行呢?(用户行为对缓存的影响)
  • 然后你开始猜想既然缓存会引起一系列问题,是不是不缓存就行呢?
  • 最后你决定找运维大佬商量,大佬问你要啥什么header?

    本文希冀

  • 了解memory cache、disk Cache

  • 了解强制缓存、协商缓存、会看Last-Modified
  • 知道自己需要的什么样的 cache-control
  • 会看304、200 from cache、200 from disk

    缓存的相关概念

  • 什么是缓存?缓存是一种保存资源副本于本地并在下次请求时直接使用该副本的技术。当 web 缓存发现请求的资源已经被存储,它会拦截请求,返回该资源的拷贝,而不会去源服务器重新下载。

  • 缓存有什么意义?加快页面打开速度,减少网络带宽消耗,降低服务器压力…
  • HTTP缓存的分类?缓存的种类有很多:
    • 根据使用对象可分为,私有与共享缓存。共享缓存存储的响应能够被多个用户使用,比如代理服务器,cdn。私有缓存只能用于单独用户,比如浏览器缓存
    • 按缓存位置分类 (memory cache, disk cache, Service Worker 等)
    • 按失效策略分类 (Cache-Control, ETag 等)
  • 读缓存的顺序

我们可以在 Chrome 的开发者工具中,Network -> Size 一列看到一个请求最终的处理方式:如果是大小 (多少 K, 多少 M 等) 就表示是网络请求,否则会列出 from memory cache, from disk cachefrom ServiceWorker
它们的优先级是:(由上到下寻找,找到即返回;找不到则继续)

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

    memory Cache

    memory cache 是内存中的缓存,(与之相对 disk cache 就是硬盘上的缓存)。按照操作系统的常理:先读内存,再读硬盘。
  • 几乎所有的网络请求资源(样式、脚本、图片)都会被浏览器自动加入到 memory cache 中。但是也正因为数量很大但是浏览器占用的内存不能无限扩大这样两个因素,memory cache 注定只能是个“短期存储”。常规情况下,浏览器的 TAB 关闭后该次浏览的 memory cache 便告失效 (为了给其他 TAB 腾出位置)。而如果极端情况下 (例如一个页面的缓存就占用了超级多的内存),那可能在 TAB 没关闭之前,排在前面的缓存就已经失效了。
  • memory cache 机制保证了一个页面中如果有两个相同的请求 (例如两个 src 相同的 📝 缓存篇 - 图1,两个 href 相同的 ) 都实际只会被请求最多一次,避免浪费。
  • 不过在匹配缓存时,除了匹配完全相同的 URL 之外,还会比对他们的类型,CORS 中的域名规则等。因此一个作为脚本 (script) 类型被缓存的资源是不能用在图片 (image) 类型的请求中的,即便他们 src 相等。
  • 在从 memory cache 获取缓存内容时,浏览器会忽视例如 max-age=0, no-cache 等头部配置。例如页面上存在几个相同 src 的图片,即便它们可能被设置为不缓存,但依然会从 memory cache 中读取。这是因为 memory cache 只是短期使用,大部分情况生命周期只有一次浏览而已。而 max-age=0 在语义上普遍被解读为“不要在下次浏览时使用”,所以和 memory cache 并不冲突。但如果是真心不想让一个资源进入缓存,就连短期也不行,那就需要使用 no-store。存在这个头部配置的话,即便是 memory cache 也不会存储,自然也不会从中读取了。
  • 200 from memory

    disk Cache

    disk cache 也叫 HTTP cache,顾名思义是存储在硬盘上的缓存,因此它是持久存储的,是实际存在于文件系统中的。而且它允许相同的资源在跨会话,甚至跨站点的情况下使用,例如两个站点都使用了同一张图片。
    disk cache 会严格根据 HTTP 头信息中的各类字段来判定哪些资源可以缓存,哪些资源不可以缓存;哪些资源是仍然可用的,哪些资源是过时需要重新请求的。当命中缓存之后,浏览器会从硬盘中读取资源,虽然比起从内存中读取慢了一些,但比起网络请求还是快了不少的。绝大部分的缓存都来自 disk cache
    200 from disk。

    Service Worker

    Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持久性的。

    Push Cache

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

    缓存过程分析

    浏览器与服务器通信的方式为应答模式,即是:浏览器发起HTTP请求 – 服务器响应该请求,那么浏览器怎么确定一个资源该不该缓存,如何去缓存呢?浏览器第一次向服务器发起该请求后拿到请求结果后,将请求结果和缓存标识存入浏览器缓存,浏览器对于缓存的处理是根据第一次请求资源时返回的响应头来确定的。
    0420086000102342107.20200917140837.48803216266734322631252967405749.png

  • 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识

  • 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中

以上两点结论就是浏览器缓存机制的关键,它确保了每个请求的缓存存入与读取。为了方便大家理解,这里我们根据是否需要向服务器重新发起HTTP请求将缓存过程分为两个部分,分别是强缓存和协商缓存。

强制缓存

强制缓存的含义是,当客户端请求后,会先访问缓存数据库看缓存是否存在。如果存在则直接返回;不存在则请求真的服务器,响应后再写入缓存数据库
强制缓存直接减少请求数,是提升最大的缓存策略。如果考虑使用缓存来优化网页性能的话,强制缓存应该是首先被考虑的。可以造成强制缓存的字段是 Cache-control 和 Expires。

协商缓存(对比缓存)

当强制缓存失效(超过规定时间)时,就需要使用协商缓存,由服务器决定缓存内容是否失效。

流程上说,浏览器先请求缓存数据库,返回一个缓存标识。之后浏览器拿这个标识和服务器通讯。如果缓存未失效,则返回 HTTP 状态码 304 表示继续使用,于是客户端继续使用缓存;如果失效,则返回新的数据和缓存规则,浏览器响应数据后,再把规则写入到缓存数据库。

协商缓存在请求数上和没有缓存是一致的,但如果是 304 的话,返回的仅仅是一个状态码而已,并没有实际的文件内容,因此 在响应体体积上的节省是它的优化点。它的优化覆盖了请求数据的三个步骤中的最后一个:“响应”。通过减少响应体体积,来缩短网络传输时间。所以和强制缓存相比提升幅度较小,但总比没有缓存好。
协商缓存是可以和强制缓存一起使用的,作为在强制缓存失效后的一种后备方案。实际项目中他们也的确经常一同出现。
对比缓存有 2 组字段(不是两个)。

强制缓存字段

Expires

这是 HTTP 1.0 的字段,表示缓存到期时间,是一个绝对的时间 (当前时间+缓存时间),如
Expires: Thu, 10 Nov 2017 08:45:11 GMT
在响应消息头中,设置这个字段之后,就可以告诉浏览器,在未过期之前不需要再次请求。
但是,这个字段设置时有两个缺点:

  • 由于是绝对时间,用户可能会将客户端本地的时间进行修改,而导致浏览器判断缓存失效,重新请求该资源。此外,即使不考虑自信修改,时差或者误差等因素也可能造成客户端与服务端的时间不一致,致使缓存失效。
  • 写法太复杂了。表示时间的字符串多个空格,少个字母,都会导致非法属性从而设置失效。

Cache-control

已知Expires的缺点之后,在HTTP/1.1中,增加了一个字段Cache-control,该字段表示资源缓存的最大有效时间,在该时间内,客户端不需要向服务器发送请求。这两者的区别就是前者是绝对时间,而后者是相对时间。
可以混合使用:

  • max-age:即最大有效时间
  • must-revalidate:如果超过了 max-age 的时间,浏览器必须向服务器发送请求,验证资源是否还有效。
  • no-cache:虽然字面意思是“不要缓存”,但实际上还是要求客户端缓存内容的,只是是否使用这个内容由后续的对比来决定。
  • no-store: 真正意义上的“不要缓存”。所有内容都不走缓存,包括强制和对比。
  • public:所有的内容都可以被缓存 (包括客户端和代理服务器, 如 CDN)
  • private:所有的内容只有客户端才可以缓存,代理服务器不能缓存。默认值。


这里有一个疑问:max-age=0 和 no-cache 等价吗?
从规范的字面意思来说,max-age 到期是 应该(SHOULD) 重新验证,而 no-cache 是 必须(MUST) 重新验证。但实际情况以浏览器实现为准,大部分情况他们俩的行为还是一致的。(如果是 max-age=0, must-revalidate 就和 no-cache 等价了)
顺带一提,在 HTTP/1.1 之前,如果想使用 no-cache,通常是使用 Pragma 字段,如 Pragma: no-cache(这也是 Pragma 字段唯一的取值)。但是这个字段只是浏览器约定俗成的实现,并没有确切规范,因此缺乏可靠性。它应该只作为一个兼容字段出现,在当前的网络环境下其实用处已经很小。
总结一下,自从 HTTP/1.1 开始,Expires 逐渐被 Cache-control 取代。Cache-control 是一个相对时间,即使客户端时间发生改变,相对时间也不会随之改变,这样可以保持服务器和客户端的时间一致性。而且 Cache-control 的可配置性比较强大。
Cache-control 的优先级高于 Expires,为了兼容 HTTP/1.0 和 HTTP/1.1,实际项目中两个字段我们都会设置。

协商缓存字段

Last-Modified & If-Modified-Since

  • 服务器通过 Last-Modified 字段告知客户端,资源最后一次被修改的时间,例如
    Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT
  • 浏览器将这个值和内容一起记录在缓存数据库中。
  • 下一次请求相同资源时时,浏览器从自己的缓存中找出“不确定是否过期的”缓存。因此在请求头中将上次的 Last-Modified 的值写入到请求头的 If-Modified-Since 字段
  • 服务器会将 If-Modified-Since 的值与 Last-Modified 字段进行对比。如果相等,则表示未修改,响应 304;反之,则表示修改了,响应 200 状态码,并返回数据。

但是他还是有一定缺陷的:
如果资源更新的速度是秒以下单位,那么该缓存是不能被使用的,因为它的时间单位最低是秒。
如果文件是通过服务器动态生成的,那么该方法的更新时间永远是生成的时间,尽管文件可能没有变化,所以起不到缓存的作用。

Etag & If-None-Match

为了解决上述问题,出现了一组新的字段 Etag 和 If-None-Match
Etag 存储的是文件的特殊标识(一般都是 hash 生成的),服务器存储着文件的 Etag 字段。之后的流程和 Last-Modified 一致,只是 Last-Modified 字段和它所表示的更新时间改变成了 Etag 字段和它所表示的文件 hash,把 If-Modified-Since 变成了 If-None-Match。服务器同样进行比较,命中返回 304, 不命中返回新资源和 200。
Etag 的优先级高于 Last-Modified

缓存小结

  • 缓存来回顺序:
    • 当浏览器要请求资源时:
      • 调用 Service Worker 的 fetch 事件响应
      • 查看 memory cache
      • 查看 disk cache。这里又细分:

如果有强制缓存且未失效,则使用强制缓存,不请求服务器。这时的状态码全部是 200
如果有强制缓存但已失效,使用协商缓存,比较后确定 304 还是 200

  • 响应顺序:
    • 发送网络请求,等待网络响应
    • 把响应内容存入 disk cache (如果 HTTP 头信息配置可以存的话)
    • 把响应内容 的引用 存入 memory cache (无视 HTTP 头信息的配置)
    • 把响应内容存入 Service Worker 的 Cache Storage (如果 Service Worker 的脚本调用了 cache.put())
      • 强制缓存减少请求个数;协商缓存请求个数与未缓存一致,但资源未修改时,不返回资源,仅有一个请求头。
      • 通过HTML的META设置expires和cache-control 控制缓存真的能生效吗?答案是否定的。参考
      • 对于不想缓存的资源,可以设置:add_header 'Cache-Control' 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
      • 公司线上配置了cache-control: max-age=0,但是不能配置cache-control了。因为据杭州运维说:线上请求是cdn先到的slb,slb再请求的nginx。也就是说即使配置nginx,nginx返回的请求头经过中间层才会到cdn,中间层是不可控的,可能不会直接转发(ugc项目证实确实如此)

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

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

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

实际场景应用缓存策略

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

2.不常变化的资源
Cache-Control: max-age=31536000
通常在处理这类资源时,给它们的 Cache-Control 配置一个很大的 max-age=31536000 (一年),这样浏览器之后请求相同的 URL 会命中强制缓存。而为了解决更新的问题,就需要在文件名(或者路径)中添加 hash, 版本号等动态字符,之后更改动态字符,从而达到更改引用 URL 的目的,让之前的强制缓存失效 (其实并未立即失效,只是不再使用了而已)。
在线提供的类库 (如 jquery-3.3.1.min.js, lodash.min.js 等) 均采用这个模式。Expires:Wed, 05 Apr 2017 00:55:35 GMT

有趣的东东

启发式算法
如果Expires, Cache-Control: max-age, 或 Cache-Control:s-maxage 都没有在响应头中出现, 并且也没有其它缓存的设置, 那么浏览器默认会采用一个启发式的算法,:

其缓存时间为 (Date_value - Last-Modified_value) * 10%,

  1. const Date_value = new Date('Fri, 17 Sep 2021 09:35:08 GMT').getTime();
  2. const LastModified_value = new Date('Tue, 24 Aug 2021 08:56:41 GMT').getTime();
  3. const cacheTime = (Date_value - LastModified_value) / 10;
  4. const Expires_timestamp = Date_value + cacheTime;
  5. const Expires_value = new Date(Expires_timestamp);
  6. console.log('Expires:', Expires_value); // Expires: Mon Sep 20 2021 03:14:58 GMT+0800 (中国标准时间)

可见该资源将于Mon Sep 20 2021 03:14:58 GMT+0800过期, 尝试以下两步进行验证:

  • 打开页面, 发现强缓存依然有效(依旧是200 OK (from disk cache))
  • 然后又修改本地时间为21日,发现缓存已过期, 此时浏览器重新向服务器发起了验证, 且命中了304协商缓存, 如下所示

可见, 启发式缓存算法采用的缓存时间可长可短, 因此对于常规资源, 建议明确设置缓存时间(如指定max-age 或 expires).

实验

默认情况下,在ng或者其他请求头什么都不配置的时候

  • 直接打开页面
  • 强制刷新请求头重会增加,no-cache
  • 普通刷新请求头会增加:max-age:0
  • 启发式缓存