关于 Web 缓存的那些风流事儿

最近大家针对preload、HTTP/2 push和ServiceWorker的浏览器缓存实现展开了激烈的讨论,而这也引起了很多人的疑惑。

鉴于此,我想讲个故事来让大家了解一个请求如何完成他的使命并找到匹配的缓存资源,

以下内容均基于 Chromium 的术语,不过其余浏览器的实现本质上没有太大的差异。

Questy 的旅程

Questy 是一个请求。她是在渲染引擎内(也叫渲染器)诞生的。她渴望能在这个标签页关闭前找到一个让她的“人生”再无遗憾的资源。

所以 Questy 展开了她追求幸福的旅程。 但是她会在哪里找到一个恰恰适合的资源呢?

此时离她最近的是……

内存缓存(Memory Cache)

内存缓存中包含了大量的资源。他包含了所有渲染引擎请求的资源。这些资源都是现有文档的一部分。在文档的生命周期中他们都会被储存在此。这意味着,如果 Questy 寻找的资源已经被文档中的其余部分加载了,那么他们会在此相遇。

确切来说,“短期内存缓存”这个名字可能会更适合。因为内容缓存仅在导航结束前保存这些资源,在某些情况下,时间甚至会更短。

事实上,很多种情况都会导致 Questy 寻找的资源已经被加载。

预加载器(preloader)可能是最常发生的情况。如果 Questy 是由 HTML 解析器创造的 DOM节点所激发的,那么她很可能会发现,她所寻找的资源早已在 HTML 标记化阶段加载完毕了。

显示 preload 指令(<link rel=preload>)则是另一种较为可能发生的情况。该指令会让浏览器预加载资源并存储在内存缓存中。

除此之外,还有可能是因为所请求的资源与之前的 DOM 节点或者 CSS 规则所需要的资源相同。例如,一个页面中可能会含有多个具有相同 src 属性的 <img> 元素,但是他们会得到同一个资源。而实现这种机制的正是内存缓存。

然而,内存缓存不会轻易匹配我们的资源请求。当然了,为了使请求和资源相匹配,他们必须要有相同的 URL 。不过,这还不是全部。他们还必须要有相同的资源类型(这样子一个脚本资源才不会被一个图片请求所匹配),相同的 CORS 策略和一些其他特性。

规范并没有十分地明确定义内存缓存所需要匹配的特性,所以不同的浏览器的实现可能会有一定的差异。

有一样东西是内存缓存不关心的,那就是 HTTP 语义。无论资源的头部是是否带有 max-age=0 或者 no-cacheCache-Control标签,内存缓存都不关心。因为在当前导航中,资源是可以重用的,所以 HTTP 语义并不重要。

唯一例外的是no-store指令。在某些特定的情况下浏览器会尊重他。(例如,当资源被单独节点重用时)。

所以,Questy 走上前询问内存缓存是否有匹配的资源。唉,然而并没有。

Questy 并没有放弃。她走过资源计时器和开发者工具的网络注册点。在那里,她注册为寻找资源的请求(这意味着如果她能找到匹配的资源,则会出现在开发中工具和资源计时器中)。

完成了这些官方登记后,她继续向前……

Service Worker 缓存

和内存缓存不一样,Service Woker喜欢不走寻常路。他的行为难以预测。因为他只遵循开发者告诉他的规则。

首先,Service Worker只有安装后才会存在。而且因为他的逻辑是由开发者编写的 JavaScript 而不是浏览器控制的,所以 Questy 完全不知道她能不能在这里找到那个他?那个资源长成什么的?他是被存储在缓存里吗?还是说他是由 Service Worker 的主人精心伪造的响应?

这些问题没有人可以回答她。因为 Service Worker 自成一套,无论是资源的匹配方式还是响应的包装方法,他们都能按照自己的的想法去完成。

Service Worker 拥有和缓存相关的 API ,这让他可以储存资源。和内存储存不同的是这种存储方式是持久的。即使该标签页被关闭甚至浏览器重启,这些被存储的资源都不会丢失。只有当开发者明确表示要移除他们的时候(使用 cache.delete(resource)),他们才会被移除。另外一种情况就是当浏览器的存储空间不足时,他会将整个 Service Worker 缓存还有其他源存储如 indexedDB、localStorage 等都清除掉。也因此,Service Worker 能确保他的存储和其他源存储是同步的。

Service Worker 只负责特定的域,换言之,他最多只能管理一个 host。因此,Service Worker 只能控制来自特定域内的文档的请求。

Questy 走向 Service Worker 询问他有没有合适的资源。可惜的是 Service Worker 从来没有见过那个域的资源,所以他也找不到 Questy 寻找的请求了。于是,Service Worker 让 Questy 继续前行(通过 fetch()),从而在网络栈这片神奇的土地里继续寻找她需要的资源。

而一旦进入网络栈,最容易找到资源的地方就是……

HTTP 缓存

HTTP 缓存(有时候也被他的朋友成为“磁盘缓存”)和 Questy 之前遇到过的缓存不太一样。

一方面,他们的存储是持久的,而且能被不同的会话甚至不同的网站重用。如果一个资源被一个网站下载了,他也可以被其他网站重用,

而另一方面,HTTP 缓存遵循 HTTP 语义(名字早已暗示了一切)。他乐于提供他认为觉得是“新鲜”的资源(基于由响应的缓存头声明的生命周期)、校验那些需要重新验证的资源、并拒绝存储那些它不应该存储的资源。

既然他是一个持久性的缓存,他也需要移除资源。但和 Service Worker 不一样的事,他会在觉得他需要空间来存储更重要或者会被更多人需要的资源时,逐个移除那些旧资源。

HTTP 缓存拥有一个基于内存的组件。他负责为请求匹配资源。可是一旦资源匹配成功,它需要从磁盘中获取资源内容,这是一个较为昂贵的操作。

上文我们提到 HTTP 缓存遵循 HTTP 语义。这基本是正确的。除了一个例外情况,HTTP 缓存会存储一些资源一段时间。浏览器能够为下次导航预取资源。我们可以通过显示的指令(<link rel=prefetch>)或者依靠浏览器内部机制完成。这些被预取的资源会被保存下来直到下次导航,尽管它们可能是不允许缓存的。所以当预取资源到达 HTTP 缓存时,它会被缓存(并且不需要校验就会被提供)大概五分钟。

尽管 HTTP 缓存看起来十分的严厉,但 Questy 还是鼓起勇气上前询问有没有匹配的资源。然而答案依旧是没有。

她还是得继续随着网络往前走。这段旅程时可怕而且未知的,然而 Questy 知道无论如何她都要找到她需要的资源。所以她只能继续。这时候她找到了一个对应的 HTTP/2 会话。并且准备通过网络继续前行,这时候她忽然看到了……

推送“缓存”

推送缓存(其实他更应该被描述为“待认领的推送流存储器”,不过那实在是太拗口了)是存储 HTTP/2 推送资源的地方。它们是 HTTP/2 会话的一部分,这有几个特殊的含义。

这个容器并不是持久的。当会话结束后,未被认领的资源(例如,从来没有被请求匹配到的)就会被移除。如果资源是由不同的 HTTP/2 会话获取的,他们并不会匹配。除此之外,推送缓存只会存储资源一段时间(在基于 Chromium 的浏览器里,这个时长约为五分钟)。

推送缓存根据请求的 URL 和请求头匹配相应,但他不遵循严格的 HTTP 语义。

规范里也没有明确定义推送缓存,所以再各个浏览器、系统或者 HTTP/2 客户端间的实现可能会不一样。

尽管信心不大,Questy 还是上前询问是否有匹配的请求。令人惊讶的是,他真的有!!Questy 喜出望外的认领了这个资源(这也意味着它将这个 HTTP/2 流从待认领容器中移除)。现在她可以回去渲染这个资源了。

在他们回程的路上,他们走过了 HTTP 缓存,并且话费了一些时间去复制了一份资源以备日后使用。

离开网络栈后,他们回到 Service Worker 的辖区,而 Service Worker 也将一份资源的拷贝存储到自己的缓存中才让他们回到渲染器里。

最终,一旦它们会到渲染器,内存缓存就会保存一份资源的引用(而不是拷贝)。这样子在稍后如果在同一个导航会话中需要这份资源,他就可以将相同的资源分配给他。

于是,它们就幸福快活的住在了一起,直到文档被移除,然后他们都被垃圾回收了。

不过那是另外一天的故事了。

原文发布于微信公众号 - java一日一条(mjx_java)

原文发表时间:2017-04-14

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏JAVA同学会

Kafka 简介

在Kafka中,客户端和服务器之间的通信是通过一种简单的,高性能的,语言不可知的TCP协议完成的。

36920
来自专栏idealclover的填坑日常

如何使用github给大佬递茶

最近也是闲到没事干(误),开始给开源项目打小黑工贡献代码,当个dalao手底下的端茶党。但是看来端茶党也不是那么容易做的或许只是傻翠他智商太低跟不上,在打小黑工...

18820
来自专栏自由而无用的灵魂的碎碎念

老电脑如果从windows7升级到windows10不断重启进不了系统,还是想用windows10,怎么办?

先说一下我的配置:08年的acer aspire 5520g,很老的电脑,除了内存加到4g,其他都不变。官方只支持到windows7,并且官方说明该型号不在官方...

10810
来自专栏java学习

针对java初学者以及自学者的一篇入门教程

Java基础 | 数据库 | Android | 学习视频 | 学习资料下载 最新通知 按照我去培训机构的学习经历,给初学还有自学Java 的同学一个基本的学习...

44390
来自专栏Debian社区

Parsix GNU/Linux 项目宣布即将终止

基于 Debian 的 Parsix 发行版已经宣布将会在 Debian Stretch 发布六个月后终止。官方表示 Parsix GNU/Linux 8.15...

10520
来自专栏JAVA同学会

Kafka 简介

在Kafka中,客户端和服务器之间的通信是通过一种简单的,高性能的,语言不可知的TCP协议完成的。

38840
来自专栏Java后端技术栈

Apache Kafka:下一代分布式消息系统

Apache Kafka是分布式发布-订阅消息系统。它最初由LinkedIn公司开发,之后成为Apache项目的一部分。Kafka是一种快速、可扩展的、设计内在...

10710
来自专栏马洪彪

应用系统数据删除与恢复

14120
来自专栏Vamei实验室

协议森林15 先生,要点单吗? (HTTP协议概览)

我在TCP流通信中说明了,TCP协议实现了数据流的传输。然而,人们更加习惯以文件为单位传输资源,比如文本文件,图像文件,超文本文档(hypertext docu...

20970
来自专栏听雨堂

Android新手之旅(1) 开发环境的安装

  大致情况:安装Android开发环境,网上有很多的教程,装eclipse,jdk…别的问题都不大,最麻烦的是装sdk,因为安装是一个在线安装过程,而要安装的...

20570

扫码关注云+社区

领取腾讯云代金券