原文地址:A Tale of Four Caches浏览器
谈到 preload,HTTP/2 push 以及 Service workers,人们都有不少看法,但也有不少困惑。 因此,我想给你讲述一个关于 HTTP 请求履行本身使命,为了匹配资源而旅行的故事。缓存
这个故事基于 Chromium 的术语与概念,在其余浏览器可能会有所不一样。
复制代码
Questy 是一个请求。它由渲染引擎(简称 renderer)在内部建立,它强烈渴望找到一个可以让它完成使命而且一直(至少到因为标签被关闭致使的文档被分离的时候)快乐地在一块儿的资源。bash
因此 Questy 启程去追寻幸福。但它会在哪找到适合它的资源呢? 最近的地方是...网络
内存缓存有一个充满资源的大容器。它包含了 renderer 获取的当前文档的全部资源,而且会在文档的生命周期内保存。这就意味着 Questy 寻找的资源若是已经在当前文档的其余地方被获取过,那么这个资源将会在内存缓存中被找到。 不过称它为「短时间内存缓存」或许更合适,由于内存缓存只在导航结束前保存资源,在某些状况下还可能更短。fetch
出现 Questy 寻找的资源已经被获取过这一状况有不少潜在缘由。网站
preloader 可能最大的一个。若是 Questy 是做为 DOM 节点被 HTML 解析器创造出来的话,那么在 preloader 的 HTML 标记化阶段中它所须要的资源颇有可能被获取了。spa
显式的 preload 的指令(<link rel=preload>
) 是另一种预加载的资源已经被存储在内存缓存中的状况。操作系统
另外,先前的 DOM 节点或者 CSS 规则也可能已经获取了相同的资源。例如,一个页面包含多个有相同 src
属性的 <img>
元素,这时只会获取一个资源。这种可以让多个元素获取一个资源的机制就是因为内存缓存的存在。.net
可是内存缓存并不会轻易让请求匹配到资源。显然,为了让请求和资源匹配,他们有匹配的 URL 还不够,必须还有匹配的资源类型,CORS 模式和一些其余特性。3d
来自内存缓存的请求的匹配特征在规范中并无很好的定义,所以在浏览器实现中可能会略有不一样。
复制代码
内存缓存也不关心 HTTP 语义。就算存储的资源有 max-age=0
或 no-cache
Cache-Control
消息头,那也不是内存缓存关心的东西。因为它容许在当前导航中重用资源,因此 HTTP 语义在这里并不重要。
惟一的例外是 no-store
指令,内存缓存在某些状况下会遵照该指令(例如,当资源被单独的节点重用时)。
Questy 继续向内存缓存寻求匹配的资源。不过一个也没找到。
但 Questy 并无放弃。它经过了 Resource Timing 和 DevTools network 注册点,在那里注册为寻找资源的请求(这意味着它如今将显示在 DevTools 以及 resource timing 中,假定它最终会找到它的资源)。
在注册这一行政部分完成后,它继续朝着...
与内存缓存不一样,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 缓存,有时也被它的缓存朋友称为「磁盘缓存」,它与 Questy 以前看到的缓存彻底不一样。
一方面,它是持久的,容许资源在会话之间甚至跨站点重用。若是某个资源由一个站点缓存,那么 HTTP 缓存也容许其余站点重用该资源。
同时,HTTP 缓存遵循 HTTP 语义(它的名字就代表了这一点)。它乐于为其认为「新鲜」的资源提供服务(基于缓存生命周期,由其响应的缓存头指示),从新验证资源,并拒绝存储不应存储的资源。
它是一个持久缓存,因此它也须要驱逐资源,但与 Service Worker 缓存不一样的是,只要缓存须要空间去储存更重要或更流行的资源时,资源就能一个接一个地被逐出。
HTTP 缓存具备一个基于内存的组件。在组件中,它会对进入的请求进行资源匹配。但当它找到了匹配的资源时,它会从磁盘中获取资源内容,而这会是一个昂贵的操做。
咱们以前提到过,HTTP 缓存尊重HTTP语义。这个说法几乎彻底正确,但有一个例外:HTTP 缓存会在有限的时间内存储资源。
经过显式的提示(`<link rel=prefetch>`)或者浏览器的内部策略,能为下一个导航预获取资源,而那些预获取的资源会保存到下次导航,即便它们是不可缓存的。
因此当这种预获取的资源到达 HTTP 缓存时,它会被缓存(并没有需从新验证)5分钟。
复制代码
HTTP缓存看起来至关严格,但 Questy 仍是鼓起勇气问它是否有匹配的资源。答案是没有。
它将不得不继续走向网络。经过网络的旅程是可怕且不可预知的,但 Questy 明白它不管如何都必须找到它的资源。因此它继续前进。它发现了一个 HTTP/2 会话,接着很快就会经过网络发送,这时它忽然看到了......
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,内存缓存保留了资源的引用(不是拷贝),以便在这个导航会话中为未来的请求分配相同的资源。
他们今后过着幸福快乐的生活。直到文档被分离,他们都会去见垃圾回收器。
但那是另外一天的故事了。
那么,咱们能从 Questy 的旅程中学到什么?
总而言之,若是你使用 preload、H2 push、Service Worker 或其余先进技术来尝试加速你的网站时,你可能会注意到内部缓存实现的状况。经过了解这些内部缓存以及它们的运行方式可能会帮助你更好地理解网站现状,并有可能避免没必要要问题。