原文地址:A Tale of Four Caches
关于四个缓存的故事
谈到 preload,HTTP/2 push 以及 Service workers,大家都有很多话要说,但也有很多困惑。
所以,我想给你讲述关于一个请求履行使命并且匹配资源的旅行的故事。
这个故事基于 Chromium 的术语与概念,在其他浏览器可能会有所不同。
Questy 的旅行
Questy 是一个请求。它由渲染引擎(简称 renderer)在内部创建,它强烈渴望找到一个能够让它完成使命并且一直(至少到由于标签被关闭导致的文档被分离的时候)快乐地在一起的资源。
所以 Questy 启程去追寻幸福。但它会在哪找到适合它的资源呢?
最近的地方是...
内存缓存
内存缓存有一个充满资源的大容器。它包含了 renderer 获取的当前文档的所有资源,并且会在文档的生命周期内保存。这就意味着 Questy 寻找的资源如果已经在当前文档的其他地方被获取过,那么这个资源将会在内存缓存中被找到。
不过称它为「短期内存缓存」或许更合适,因为内存缓存只在导航结束前保存资源,在某些情况下还可能更短。
出现 Questy 寻找的资源已经被获取过这一情况有很多潜在原因。
preloader 可能最大的一个。如果 Questy 是作为 DOM 节点被 HTML 解析器创造出来的话,那么在 preloader 的 HTML 标记化阶段中它所需要的资源很有可能被获取了。
显式的 preload 的指令() 是另外一种预加载的资源已经被存储在内存缓存中的情况。
另外,先前的 DOM 节点或者 CSS 规则也可能已经获取了相同的资源。例如,一个页面包含多个有相同 src
属性的 元素,这时只会获取一个资源。这种能够让多个元素获取一个资源的机制就是由于内存缓存的存在。
但是内存缓存并不会轻易让请求匹配到资源。显然,为了让请求和资源匹配,他们有匹配的 URL 还不够,必须还有匹配的资源类型,CORS 模式和一些其他特性。
[图片上传失败...(image-575f4b-1520847010388)]
来自内存缓存的请求的匹配特征在规范中并没有很好的定义,因此在浏览器实现中可能会略有不同。
内存缓存也不关心 HTTP 语义。就算存储的资源有 max-age=0
或 no-cache
Cache-Control
消息头,那也不是内存缓存关心的东西。由于它允许在当前导航中重用资源,所以 HTTP 语义在这里并不重要。
[图片上传失败...(image-c00a4c-1520847010388)]
唯一的例外是 no-store
指令,内存缓存在某些情况下会遵守该指令(例如,当资源被单独的节点重用时)。
Questy 继续向内存缓存寻求匹配的资源。不过一个也没找到。
但 Questy 并没有放弃。它通过了 Resource Timing 和 DevTools network 注册点,在那里注册为寻找资源的请求(这意味着它现在将显示在 DevTools 以及 resource timing 中,假定它最终会找到它的资源)。
在注册这一行政部分完成后,它继续朝着...
Service Worker 缓存
与内存缓存不同,Service Worker 缓存并不遵循任何传统规则。它只遵守他们的主人(即 Web 开发者)告诉它的规则。因此在某种程度上它是不可预测的。
首先,只有当页面安装了 Service Worker,它才会存在。而且由于它的逻辑不是内置于浏览器,而是由 Web 开发者通过 JavaScript 定义的,Questy 并不知道它是否愿意为自己寻找资源,即便它愿意,这资源就是它所梦寐以求的吗?它会是存储在它的缓存中的匹配资源吗?还是只是由 Service Worker 的主人的扭曲逻辑所创建的一个响应?
没有人知道。因为 Service Workers 拥有自己的逻辑,所以他们可以任意地完成匹配请求和潜在资源、包装 Response 对象中这些行为。
Service Worker 有一个使它能够保留资源的 Cache API。它和内存缓存之间的一个主要区别是它是持久的。即使选项卡关闭或浏览器重新启动,存储在该缓存中的资源仍会保留。他们会从缓存中被逐出的一种情况是,开发者明确将他们逐出(使用cache.delete(resource)
)。另一种情况是,浏览器用完了存储空间,在这种情况下,整个 Service Worker 缓存会与所有其他原始存储,如indexedDB、localStorage 等,一起被删除。这样,Service Worker 就保持在它缓存中的资源与它自身以及其他原始存储之间同步。
Service Worker 负责最多一个主机的范围。因此 Service Worker 只能对该范围内的文档请求进行响应。
Questy 找到了 Service Worker 并问它是否有资源。但 Service Worker 从来没有在自己掌管范围内的看到它要的资源,所以没有相应的资源给予。因此,Service Worker 派遣(使用fetch()
) Questy 继续在网络堆栈的未知大陆上搜索资源。
而在网络堆栈中,寻找资源的最好地方就是...
HTTP 缓存
HTTP 缓存,有时也被它的缓存朋友称为「磁盘缓存」,它与 Questy 之前看到的缓存完全不同。
一方面,它是持久的,允许资源在会话之间甚至跨站点重用。如果某个资源由一个站点缓存,那么 HTTP 缓存也允许其他站点重用该资源。
同时,HTTP 缓存遵循 HTTP 语义(它的名字就表明了这一点)。它乐于为其认为「新鲜」的资源提供服务(基于缓存生命周期,由其响应的缓存头指示),重新验证资源,并拒绝存储不该存储的资源。
它是一个持久缓存,所以它也需要驱逐资源,但与 Service Worker 缓存不同的是,只要缓存需要空间去储存更重要或更流行的资源时,资源就能一个接一个地被逐出。
HTTP 缓存具有一个基于内存的组件。在组件中,它会对进入的请求进行资源匹配。但当它找到了匹配的资源时,它会从磁盘中获取资源内容,而这会是一个昂贵的操作。
我们之前提到过,HTTP 缓存尊重HTTP语义。这个说法几乎完全正确,但有一个例外:HTTP 缓存会在有限的时间内存储资源。通过显式的提示(``)或者浏览器的内部策略,能为下一个导航预获取资源,而那些预获取的资源会保存到下次导航,即使它们是不可缓存的。所以当这种预获取的资源到达 HTTP 缓存时,它会被缓存(并无需重新验证)5分钟。
HTTP缓存看起来相当严格,但 Questy 还是鼓起勇气问它是否有匹配的资源。答案是没有。
它将不得不继续走向网络。通过网络的旅程是可怕且不可预知的,但 Questy 明白它无论如何都必须找到它的资源。所以它继续前进。它发现了一个 HTTP/2 会话,接着很快就会通过网络发送,这时它突然看到了......
Push 「Cache」
Push 缓存(称为「unclaimed push streams container」或许更合适,但不那么容易上手)是存储 HTTP/2 推送资源的地方。它们作为 HTTP/2 会话的一部分进行存储,并具有多种含义。
该容器没有任何持久性。如果会话被终止,那么没有被请求的所有资源都会消失。如果使用不同的 HTTP/2 会话获取资源,它将不会匹配。最重要的是,资源只在有限的时间内保存在 push 缓存容器中。(在基于 Chromium 的浏览器中约5分钟)
push 缓存根据其URL以及其各种请求头匹配请求与资源,但它不适用严格的HTTP语义。
push 缓存在规范中也没有很好的定义,实现可能因浏览器、操作系统和其他 HTTP/2 客户端而异。
Questy 没报太大希望,但它仍然询问 Push 缓存是否有匹配它的资源。而令人惊讶的是,它有资源!Questy 非常开心地接受了资源(这意味着它从无人认领的容器中删除了 HTTP/2 流)。现在它可以带着资源回到 renderer 中去了。
在他们回来的路上,他们通过 HTTP 缓存,HTTP 缓存让他们停下,并拿了一份资源的拷贝存储着,以防将来的请求需要它。
当他们离开网络堆栈返回到 Service Worker 中,Service Worker 也储存了一份资源拷贝,之后再送他们回到 renderer。
终于,他们回到了 renderer,内存缓存保留了资源的引用(不是拷贝),以便在这个导航会话中为将来的请求分配相同的资源。
[图片上传失败...(image-ad4e1e-1520847010388)]
他们从此过着幸福快乐的生活。直到文档被分离,他们都会去见垃圾回收器。
[图片上传失败...(image-781fd-1520847010388)]
但那是另一天的故事了。
结论
那么,我们能从 Questy 的旅程中学到什么?
- 不同的请求可以通过浏览器的不同缓存中的资源进行匹配。
- 与请求资源所匹配的缓存可能会影响 DevTools 和 Resource Timing 中显示的方式。
- 推送的资源不会永久存储,除非它们的流被请求接受。
- 不可缓存的预加载资源将不会用于下一个导航。这是 preload 和 preftech 的主要区别之一。
- 这里边还有许多不明确的地方,观察到行为可能也会因浏览器实现而有所不同。我们需要解决这个问题。
总而言之,如果你使用 preload、H2 push、Service Worker 或其他先进技术来尝试加速你的网站时,你可能会注意到内部缓存实现的情况。通过了解这些内部缓存以及它们的运行方式可能会帮助你更好地理解网站现状,并有可能避免不必要问题。