大家都知道缓存的英文叫做 cache。但我发现一个有趣的现象:这个单词在不同人的口中有不同的读音。为了全面了解缓存,我们得先从读音开始,这样才能够在和其他同事(例如 PM)交(zhuāng)流(bī)时体现自己的修(bī)养(gé)。
友情提示:文章有些长,您可能需要分批次读完,当中可以喝个咖啡或者啤酒当作中场休息。
cache 怎么念
在国外 IT 圈和大部分国外视频中,cache 的发音是 /kæʃ/(同 cash),这也是一个广泛认可的发音。但我发现在中国的 IT 圈还有相当一部分程序员(比如我自己……)读作 /kætʃ/(同 catch)。虽然不太正确,但并不妨碍互相交流。(不过为了纯正,还是应该向正确的方向靠拢)
此外还有一些小众的读法,例如 /keɪʃ/(同 kaysh),甚至 /kæʃeɪ/(像个法语发音,重音在后面)。这些因为太小众了,可能会引起沟通障碍,估计只有在特定场合或者特定圈子才能顺畅使用。
前端缓存/后端缓存
扯了些没用的,我们先进入定义环节:什么是前端缓存?与之相对的什么又是后端缓存?
基本的网络请求就是三个步骤:请求,处理,响应。
后端缓存主要集中于“处理”步骤,通过保留数据库连接,存储处理结果等方式缩短处理时间,尽快进入“响应”步骤。当然这不在本文的讨论范围之内。
而前端缓存则可以在剩下的两步:“请求”和“响应”中进行。在“请求”步骤中,浏览器也可以通过存储结果的方式直接使用资源,直接省去了发送请求;而“响应”步骤需要浏览器和服务器共同配合,通过减少响应内容来缩短传输时间。这些都会在下面进行讨论。
本文主要包含
- 按缓存位置分类 (memory cache, disk cache, Service Worker 等)
- 按失效策略分类 (Cache-Control, ETag 等)
- 帮助理解原理的一些案例
- 缓存的应用模式
按缓存位置分类
我看过的大部分讨论缓存的文章会直接从 HTTP 协议头中的缓存字段开始,例如 Cache-Control, ETag, max-age 等。但偶尔也会听到别人讨论 memory cache, disk cache 等。那这两种分类体系究竟有何关联?是否有交叉?(我个人认为这是本文的最大价值所在,因为在写之前我自己也是被两种分类体系搞的一团糟)
实际上,HTTP 协议头的那些字段,都属于 disk cache 的范畴,是几个缓存位置的其中之一。因此本着从全局到局部的原则,我们应当先从缓存位置开始讨论。等讲到 disk cache 时,才会详细讲述这些协议头的字段及其作用。
我们可以在 Chrome 的开发者工具中,Network -> Size 一列看到一个请求最终的处理方式:如果是大小 (多少 K, 多少 M 等) 就表示是网络请求,否则会列出 from memory cache, from disk cache 和 from ServiceWorker。
它们的优先级是:(由上到下寻找,找到即返回;找不到则继续)
- Service Worker
- Memory Cache
- Disk Cache
- 网络请求
memory cache
memory cache 是内存中的缓存,(与之相对 disk cache 就是硬盘上的缓存)。按照操作系统的常理:先读内存,再读硬盘。
disk cache 将在后面介绍 (因为它的优先级更低一些),这里先讨论 memory cache。
几乎所有的网络请求资源都会被浏览器自动加入到 memory cache 中。但是也正因为数量很大但是浏览器占用的内存不能无限扩大这样两个因素,memory cache 注定只能是个“短期存储”。常规情况下,浏览器的 TAB 关闭后该次浏览的 memory cache 便告失效 (为了给其他 TAB 腾出位置)。而如果极端情况下 (例如一个页面的缓存就占用了超级多的内存),那可能在 TAB 没关闭之前,排在前面的缓存就已经失效了。
刚才提过,几乎所有的请求资源 都能进入 memory cache,这里细分一下主要有两块:
- preloader。如果你对这个机制不太了解,这里做一个简单的介绍,详情可以参阅这篇文章。
熟悉浏览器处理流程的同学们应该了解,在浏览器打开网页的过程中,会先请求 HTML 然后解析。之后如果浏览器发现了 js, css 等需要解析和执行的资源时,它会使用 CPU 资源对它们进行解析和执行。在古老的年代(大约 2007 年以前),“请求 js/css - 解析执行 - 请求下一个 js/css - 解析执行下一个 js/css” 这样的“串行”操作模式在每次打开页面之前进行着。很明显在解析执行的时候,网络请求是空闲的,这就有了发挥的空间:我们能不能一边解析执行 js/css,一边去请求下一个(或下一批)资源呢?
这就是 preloader 要做的事情。不过 preloader 没有一个官方标准,所以每个浏览器的处理都略有区别。例如有些浏览器还会下载 css 中的 @import 内容或者 - preload (虽然看上去和刚才的 preloader 就差了俩字母)。实际上这个大家应该更加熟悉一些,例如 。这些显式指定的预加载资源,也会被放入 memory cache 中。
memory cache 机制保证了一个页面中如果有两个相同的请求 (例如两个 src 相同的 ,两个 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 也不会存储,自然也不会从中读取了。(后面的第二个示例有关于这点的体现)
disk cache
disk cache 也叫 HTTP cache,顾名思义是存储在硬盘上的缓存,因此它是持久存储的,是实际存在于文件系统中的。
而且它允许相同的资源在跨会话,甚至跨站点的情况下使用,例如两个站点都使用了同一张图片。
disk cache 会严格根据 HTTP 头信息中的各类字段来判定哪些资源可以缓存,哪些资源不可以缓存;哪些资源是仍然可用的,哪些资源是过时需要重新请求的。当命中缓存之后,浏览器会从硬盘中读取资源,虽然比起从内存中读取慢了一些,但比起网络请求还是快了不少的。绝大部分的缓存都来自 disk cache。
关于 HTTP 的协议头中的缓存字段,我们会在稍后进行详细讨论。
凡是持久性存储都会面临容量增长的问题,disk cache 也不例外。在浏览器自动清理时,会有神秘的算法去把“最老的”或者“最可能过时的”资源删除,因此是一个一个删除的。不过每个浏览器识别“最老的”和“最可能过时的”资源的算法不尽相同,可能也是它们差异性的体现。
Service Worker
上述的缓存策略以及缓存/读取/失效的动作都是由浏览器内部判断 & 进行的,我们只能设置响应头的某些字段来告诉浏览器,而不能自己操作。举个生活中去银行存/取钱的例子来说,你只能告诉银行职员,我要存/取多少钱,然后把由他们会经过一系列的记录和手续之后,把钱放到金库中去,或者从金库中取出钱来交给你。
但 Service Worker 的出现,给予了我们另外一种更加灵活,更加直接的操作方式。依然以存/取钱为例,我们现在可以绕开银行职员,自己走到金库前(当然是有别于上述金库的一个单独的小金库),自己把钱放进去或者取出来。因此我们可以选择放哪些钱(缓存哪些文件),什么情况把钱取出来(路由匹配规则),取哪些钱出来(缓存匹配并返回)。当然现实中银行没有给我们开放这样的服务。
Service Worker 能够操作的缓存是有别于浏览器内部的 memory cache 或者 disk cache 的。我们可以从 Chrome 的 F12 中,Application -> Cache Storage 找到这个单独的“小金库”。除了位置不同之外,这个缓存是永久性的,即关闭 TAB 或者浏览器,下次打开依然还在(而 memory cache 不是)。有两种情况会导致这个缓存中的资源被清除:手动调用 API cache.delete(resource) 或者容量超过限制,被浏览器全部清空。
如果 Service Worker 没能命中缓存,一般情况会使用 fetch() 方法继续获取资源。这时候,浏览器就去 memory cache 或者 disk cache 进行下一次找缓存的工作了。注意:经过 Service Worker 的 fetch() 方法获取的资源,即便它并没有命中 Service Worker 缓存,甚至实际走了网络请求,也会标注为 from ServiceWorker。这个情况在后面的第三个示例中有所体现。
请求网络
如果一个请求在上述 3 个位置都没有找到缓存,那么浏览器会正式发送网络请求去获取内容。之后容易想到,为了提升之后请求的缓存命中率,自然要把这个资源添加到缓存中去。具体来说:
1. 根据 Service Worker 中的 handler 决定是否存入 Cache Storage (额外的缓存位置)。
2. 根据 HTTP 头部的相关字段(Cache-control, Pragma 等)决定是否存入 disk cache
3. memory cache 保存一份资源 的引用,以备下次使用。
按失效策略分类
memory cache 是浏览器为了加快读取缓存速度而进行的自身的优化行为,不受开发者控制,也不受 HTTP 协议头的约束,算是一个黑盒。Service Worker 是由开发者编写的额外的脚本,且缓存位置独立,出现也较晚,使用还不算太广泛。所以我们平时最为熟悉的其实是 disk cache,也叫 HTTP cache (因为不像 memory cache,它遵守 HTTP 协议头中的字段)。平时所说的强制缓存,对比缓存(也叫协商缓存),以及 Cache-Control 等,也都归于此类。