课外加餐:1 | 浏览上下文组:如何计算 Chrome 中渲染进程的个数?
前言:该篇说明:请见 说明 —— 浏览器工作原理与实践 目录
在前面《04 | 导航流程》中讲过,在默认情况下,如果打开一个标签页,那么浏览器会默认为其创建一个渲染进程。不过还介绍了同一站点的概念,如果从一个标签页A 中打开了另一个新标签页B,当新标签页B 和当前标签页A 属于同一站点的话,那么新标签页B 会复用当前标签页A 的渲染进程。
具体地讲,如果从极客邦 (www.geekbang.org) 的标签页中打开新的极客时间 (time.geekbang.org) 标签页,由于这两个标签页属于同一站点(相同协议、相同根域名),所以它们会共用同一个渲染进程。
同源: 协议 + 域名 + 端口完全相同。
同一站点:协议 + 域名相同。
同一站点能保证安全性,然后放开限制条件更好的进行资源复用。
参考下图 Chrome 的任务管理器截图: chrome 任务管理器 : shift + esc
多个标签页运行在同一个渲染进程
从上图中可以看出,极客邦官网和极客时间标签页都共用同一个渲染进程,该进程 ID 是 84748。
下图是自己电脑上打开的任务管理器,左侧一个共用同一个蓝色区域,表示共用同一个渲染进程:
自己电脑上的 任务管理器
不过如果分别打开这两个标签页,比如先打开A (极客邦的)标签页,然后再新建一个标签页B,再在这个标签页中打开极客时间,这时候可以看到这两个标签页分别使用了两个不同的渲染进程,如下图:
多个标签页运行在不同的渲染进程中
标签页A:极客邦 www.geekbang.org,标签页B:极客时间 time.geekbang.org —— 同一站点:相同协议,相同域名。
为什么从 标签页 A 中 打开 标签页B会使用同一个渲染进程,而分别打开这两个标签页又会分别使用不同的渲染进程?
标签页之间的连接
要搞清楚这个问题,就先来分析下浏览器标签页之间的连接关系。
我们知道,浏览器标签页之间是可以通过 JS 脚本 来连接的,通常情况下有以下几种连接方式:
第一种是通过<a>标签来和新标签建立连接,这种方式最熟悉,如下:
<a href="https://time.geekbang.org/" target="_blank" class="">极客时间</a>
点击该链接会打开新的标签页,新标签页中的 window.opener 的值就是指向极客邦标签页中的 window,这样就可以在新的极客时间标签页中通过 opener 来操作上个极客邦的标签页了。这样就可以说,这两个标签页是有连接的。
另外,还可以通过 JS 中的 window.open 方法来和新标签页建立连接,如下:
new_window = window.open("http://time.geekbang.org")
通过上面这种方式,可以在当前标签页中通过 new_window 来控制新标签页,还可以在新标签页中通过 window.opener 来控制当前标签页。所以也可以说,如果从 A 标签页中通过 window.open 的方式打开 B 标签页,那 A 和 B 标签页也是有连接的。
通过上面两种方式打开的新标签页,不论这两个标签页是否属于同一站点,它们之间都能通过 opener 来建立连接,所以它们之间是有联系的。在 WhatWG 规范中,把这一类具有相互连接关系的标签页称为浏览上下文组(browsing context group)。
WhatWG : 网页超文本应用技术工作小组。 以推动网络 HTML5 标准为目的而成立的组织。
为什么有了 W3C,还要有 WhatWG ?
WhatWG 成立的原因是 W3C 意图放弃 HTML,而力图发展 XML。
既然提到浏览上下文组,就有必要提下浏览上下文,通常情况下,把一个标签页所包含的内容,诸如 window 对象,历史记录,滚动条位置等信息称为浏览上下文。这些通过脚本相互连接起来的浏览上下文就是浏览上下文组。规范文档。
也就是说,如果从一个标签页A 中,通过链接打开了多个新的标签页,不管这几个新的标签页是否是同一站点,它们都和标签页A 构成了浏览上下文组,因为这些标签页中的 opener 都指向了 标签页 A。
Chrome 浏览器会将浏览上下文组中属于同一站点的标签分配到同一个渲染进程中,这是因为如果一组标签页,既在同一个浏览上下文组中,又属于同一站点,那么它们可能需要在对方的标签页中执行脚本。因此,它们必须运行在同一渲染进程中。
现在清楚了浏览器是怎么分配渲染进程的了,接下来就来分析文章开头提的那个问题了:
既然都是同一站点,为什么从 A 标签页中打开 B 标签页,就会使用同一个渲染进程?而分别打开这两个标签页,又会分别使用不同的渲染进程?
首先来看第一种,在极客邦标签页内部通过链接打开极客时间标签页,那么极客时间标签页和极客邦标签页属于同一个浏览上下文组,且它们属于同一站点,所以浏览器会将它们分配到同一个渲染进程之中。
而第二种情况就简单多了,因为第二个标签页中并没有第一个标签页中的任何信息,第一个标签页也不包含任何第二个标签页中的信息,所以它们不属于同一个浏览上下文组,因此即便它们属于同一站点,也不会运行在同一个渲染进程之中。参考下图的计算标签页的流程图:
计算标签页使用的渲染进程数目
一个“例外”
现在清楚了 Chrome 浏览器为标签页分配渲染进程的策略了:
1. 如果两个标签页都位于同一个浏览上下文组,且属于同一站点,那这两个标签页会被浏览器分配到同一个渲染进程中。
2. 如果这两个条件不能同时满足,那这两个标签页会分别使用不同的渲染进程来渲染。
现在可以想下,如果从 A 标签页中打开 B 标签页,那能肯定 A 标签页和 B 标签页属于同一浏览上下文组吗?
答案是“不能”,下面来看下这个问题:
https://linkmarket.aliyun.com 内新开的标签页都是新开一个渲染进程,能帮忙解释下吗?
我们先来复现下所描述的现象,首先打开 linkmarket.aliyun.com 这个标签页,再在这个标签页中随便点击两个链接,然后就打开了两个新的标签页了,如下所示:
“例外”情况
通过 A 标签页中的链接打开了两个新标签页,B 和 C,而且也可以看出来,A、B、C 三个标签页都属于同一站点,正常情况下,它们应该共用同一个渲染进程,不过通过上图可以看出,A、B、C 三个标签页分别使用了三个不同的渲染进程。
既然属于同一站点,又不在同一个渲染进程中,所以可以推断这三个标签页不属于同一个浏览上下文组,那么接下来的分析思路就很清晰了:
1. 首先验证这三个标签页是不是真的不在同一个浏览上下文组中;
2. 然后再分析它们为什么不在同一浏览上下文组。
为了验证猜测,可以通过控制台,来看看 B 标签页和 C 标签页的 opener 的值,结果发现这两个标签页中的 opener 的值都是 null(打印 window 查看 window.opener ,如果没有浏览上文,会显示 null ),这就确定了 B、C 标签页和 A 标签页没有连接关系,当然也就不属于同一浏览上下文组了。
验证了猜测,接下来就来查查,阿里的这个站点是不是采用了什么特别的手段,移除了这两个标签页之间的连接关系。
可以看看实现链接的 HTML 文件,如下所示:
链接使用了 rel = noopener
通过上图可以发现, a 链接的 rel 属性值都使用了 noopener 和 noreferrer,通过 noopener 能猜测到这两个值是让被链接的标签页和当前标签页不要产生连接关系。
通常,将 noopener 的值引入 rel 属性中,就是告诉浏览器通过这个链接打开的标签页中的 opener 值设置为 null,引入 noreferrer 是告诉浏览器,新打开的标签页不要有引入关系。
到这里就知道了,通过 linkmarket.aliyun.com 标签页打开新的标签页要使用单独的一个进程,是因为使用了 rel=noopener 的属性,所以新打开的标签页和现在的标签页就没有了引用关系,当然它们也就不属于同一浏览上下文组了。这也就解答了上面的问题。
站点隔离
上面都是基于标签页来分析渲染进程的,不过在《35 | 安全沙箱》中介绍过,目前 Chrome 浏览器已经默认实现了站点隔离的功能,这意味着标签页中的 iframe 也会遵守同一站点的分配原则,如果标签页中的 iframe 和标签页是同一站点,并且有连接关系,那么标签页依然会和当前标签页运行在同一个渲染进程中,如果 iframe 和标签页不属于同一站点,那么 iframe 会运行在单独的渲染进程中。
站点隔离:是 chrome 浏览器中一项旨在减少 Spectre 攻击的全新功能。
通常, chrome 会把一个标签默认为一个进程,但当页面之间存在共享内容时,彼此可以共享同一个进程。而 网站隔离可消除共享进程,确保不同网站在不同进程上,以此防止发生类似 幽灵 (Spectre) 和 熔毁 (Meltdown) 的攻击。 要了解 站点隔离的具体信息 和 Spectre / Meltdown ,看 35 章 - 安全沙箱。
先来看下面这个例子:
<html> <head> <title>站点隔离:demo</title> <style> iframe { width: 800px; height: 300px; } </style> </head> <body> <div><iframe src="iframe.html"></iframe></div> <div><iframe src="https://www.infoq.cn/"></iframe></div> <div><iframe src="https://time.geekbang.org/"></iframe></div> <div><iframe src="https://www.geekbang.org/"></iframe></div> </body> </html>
在 Chrome 浏览器中打开上面这个标签页,然后观察 Chrome 的任务管理,会发现这个标签页使用了四个渲染进程,如下图:
iframe 使用单独的渲染进程
结合上图和 HTML 代码可以发现,由于 InfoQ、极客邦两个 iframe 与 父标签页不属于同一站点,所以它们会被分配到不同的渲染进程中,而 iframe.html 和源标签页属于同一站点,所以它会和源标签页运行在同一个渲染进程中 (此处说的好像不太对,四个标签看进程id 明显不同,为什么说 iframe 和 源标签页 在同一个渲染进程?上图 任务管理器可能不能很好的理解,看下图自己电脑上的任务管理器就能很好的理解了)。
自己电脑上的任务管理器
从上图中可以看到,四个标签页,但是在 任务管理器中只看到 三个 进程,其中 iframe.html 的进程 并没有展示出来。所以可以这样理解,因为 iframe.html 的标签页 和 源标签页 又同属于一个站点,所以 是共用的同一个进程。
参考下图的渲染进程数目的流程图:
计算 iframe 所使用的渲染进程数目
总结
本文的主要内容:
首先使用了两种不同的方式打开两个标签页,第一种是从 A 标签页中通过链接打开了 B 标签页,第二种是分别打开 A 和 B 标签页,这两种情况下的 A 和 B 都属于同一站点。
通过 Chrome 的任务管理器发现,虽然 A 标签页 和 B 标签页都属于同一站点,不过通过第一种方式打开的 A 标签页 和 B 标签页 会共用同一个渲染进程,而通过第二种方式打开的两个标签页却分别使用了两个不同的渲染进程。
这是因为,使用同一个渲染进程需要满足两个条件:首先 A 标签页 和 B 标签页属于同一站点,其次 A 标签页 和 B 标签页需要有连接关系。
接着分析了一个“例外”,如果在链接中加入了 rel=noopener 属性,那么通过链接打开的新标签页和源标签页之间就不会建立连接关系了。
最后还分析了站点隔离对渲染进程个数的影响,如果 A 标签页中的 iframe 和 A 标签页属于同一站点,那么该 iframe 和 A 标签页会共用同一个渲染进程,如果不是,则该 iframe 会使用单独的渲染进程。
现在应该会计算渲染进程的个数了。
在最后还要补充下同源策略对同一站点的限制,虽然 Chrome 会让有连接且属于同一站点的标签页运行在同一个渲染进程中,不过如果 A 标签页 和 B 标签页属于同一站点,却不属于同源站点,那么依然无法通过 opener 来操作父标签页中的 DOM,这依然会受到同源策略的限制。
简单地讲,极客邦和极客时间属于同一站点,但是它们并不是同源的,因为同源是需要相同域名的,虽然根域名 geekbang.org 相同,但是域名却是不相同的,一个是 time.geekbang.org,一个是 www.geekbang.org,因此浏览器判断它们不是同源的,所以依然无法通过 time.geekbang.org 标签页中的 opener 来操作 www.geekbang.org 中的 DOM。
思考题
你认为 Chrome 为什么使用同一站点划分渲染进程,而不是使用同源策略来划分渲染进程?
记录
1、阿里为什么要把同一站点的 tab 签做成无连接的,会避免什么安全隐患?
作者回复:如果多个标签在同一个进程中,那么一个标签沦陷了,其它标签都会沦陷的
2、同源要求协议、域名以及端口均一样才行;同一站点只要求协议,根域名相同即可。也就是同源的要求太严格,导致复用同一渲染进程的条件比较难满足,所有条件放宽至同一站点?
作者回复:第一原因是通常同一站点安全性是有保障的,第二个原因就是你提到的资源的复用了。
3、是不是在多标签页时,同一站点比同源能有效节约进程?
作者回复:这也是一个原因