架构
过程和线程
过程能够被形容为是一个利用的执行程序。
线程存在于过程并执行程序任意局部。
启动利用时会创立一个过程。程序兴许会创立一个或多个线程来帮忙它工作,这是可选的。
操作系统为过程提供了一个能够应用的“一块”内存,所有应用程序状态都保留在该公有内存空间中。
敞开应用程序时,相应的过程也会隐没,操作系统会开释内存。
过程能够申请操作系统启动另一个过程来执行不同的工作。
此时,内存中的不同局部会分给新过程。如果两个过程须要对话,他们能够通过 过程间通信(IPC)来进行。
许多利用都是这样设计的,所以如果一个工作过程失去响应,该过程就能够在不进行应用程序不同局部的其余过程运行的状况下重新启动。
浏览器架构
那么如何通过过程和线程构建 web 浏览器呢?它可能由一个领有很多线程的过程,或是一些通过 IPC 通信的不同线程的过程。
在 2016 年,Chrome 官网团队应用“面向服务的架构”(Services Oriented Architecture,简称 SOA)的思维设计了新的 Chrome 架构。
如果在资源受限的设施上(如下图),Chrome 会将很多服务整合到一个过程中,从而节俭内存占用。
过程 | 管制 |
---|---|
浏览器过程 | 次要负责界面显示、用户交互、子过程治理,同时提供存储等性能。 |
渲染过程 | 外围工作是将 HTML、CSS 和 JavaScript 转换为用户能够与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该过程中,默认状况下,Chrome 会为每个 Tab 标签创立一个渲染过程。出于平安思考,渲染过程都是运行在沙箱模式下。 |
插件过程 | 次要是负责插件的运行,因插件易解体,所以须要通过插件过程来隔离,以保障插件过程解体不会对浏览器和页面造成影响。 |
网络过程 | 次要负责页面的网络资源加载,之前是作为一个模块运行在浏览器过程外面的,直至最近才独立进去,成为一个独自的过程。 |
GPU 过程 | 在独立的过程中解决 GPU 工作。之所以放到独立的过程,是因为 GPU 要解决来自多个利用的申请,但要在同一个界面上绘制图形。 |
SOA 架构的长处
-
稳固且晦涩
因为过程是互相隔离的,所以当一个页面或者插件解体时,影响到的仅仅是以后的页面过程或者插件过程,并不会影响到浏览器和其余页面,这就完满地解决了页面或者插件的解体会导致整个浏览器解体,也就是不稳固的问题。
同理,JavaScript 也是运行在渲染过程中的,所以即便 JavaScript 阻塞了渲染过程,影响到的也只是以后的渲染页面,而并不会影响浏览器和其余页面。
-
平安沙箱
SOA 架构还有助于平安和隔离。因为操作系统有限度过程特权的机制,浏览器能够借此限度某些过程的能力。
比方,Chrome 会限度解决任意用户输出的渲染器过程,不让它任意拜访文件。
-
更内聚,松耦合,易于保护和扩大
原来的各种模块被重形成独立的服务(Service),每个服务(Service)都能够在独立的过程中运行,拜访服务(Service)必须应用定义好的接口,通过 IPC 来通信,从而构建一个更内聚、松耦合、易于保护和扩大的零碎,更好实现 Chrome 简略、稳固、高速、平安的指标。
站点隔离
站点隔离 (Site Isolation for web developers) 是早先引入 Chrome 的一个里程碑式个性,即每个跨站点 iframe 都运行一个独立的渲染器过程。
即使像后面说的那样,每个标签页单开一个渲染器过程,但容许跨站点的 iframe 运行在同一个渲染器过程中并共享内存空间,那平安攻打依然有可能绕开同源策略,而且有人发现在古代 CPU 中,过程有可能读取任意内存(Meltdown/Spectre)。
过程隔离是隔离站点、确保上网平安最无效的形式。
Chrome 默认采纳站点隔离。站点隔离是多年工程化致力的后果,它并非多开几个渲染器过程那么简略。
比方,不同的 iframe 运行在不同过程中,开发工具在后盾依然要做到无缝切换,而且即使简略地 Ctrl+F
查找也会波及在不同过程中搜寻。
导航
导航波及浏览器过程与线程间为显示网页而通信。所有从用户在浏览器中输出一个 URL 开始。输出 URL 之后,浏览器会通过互联网获取数据并显示网页。从申请网页到浏览器筹备渲染网页的过程,叫做导航。
上面咱们逐渐看一看导航的几个步骤。
第一步:解决输出。
UI 线程会判断用户输出的是查问字符串还是 URL。因为 Chrome 的地址栏同时也是搜寻框。
第二步:开始导航。
如果输出的是 URL,首先,网络过程会查找本地缓存是否缓存了该资源。
如果有缓存资源,那么间接返回资源给浏览器过程。
如果在缓存中没有查找到资源,那么 UI 线程会告诉网络线程发动网络调用,获取网站内容。
此时标签页左端显示旋转图标,网络线程进行 DNS 查问、建设 TLS 连贯(对于 HTTPS)。
网络线程可能收到服务器的重定向头部,如 HTTP 301。此时网络线程会跟 UI 线程沟通,通知它服务器要求重定向。而后,再发动对另一个 URL 的申请。
第三步:读取响应。
服务器返回的响应体到来之后,网络线程会查看接管到的前几个字节。响应的 Content-Type 头部应该蕴含数据类型,如果没有这个字段,则须要 MIME 类型嗅探。
如果响应是 HTML 文件,那下一步就是把数据交给渲染器过程。但如果是一个 zip 文件或其余文件,那就意味着是一个下载申请,须要把数据传给下载管理器。
此时也是“平安浏览”查看的环节。如果域名和响应数据匹配已知的歹意网站,网络线程会显示正告页。
此外,CORB (Cross Origin Read Blocking) 查看也会执行,以确保敏感的跨站点数据不会发送给渲染器过程。
第四步:分割渲染器过程。
所有查检结束,网络线程确认浏览器能够导航到用户申请的网站,于是会告诉 UI 线程数据曾经筹备好了。UI 线程会分割渲染器过程渲染网页。
关上一个新页面采纳的渲染过程策略:
- 通常状况下,关上新的页面都会应用独自的渲染过程;
-
如果从 A 页面关上 B 页面,且 A 和 B 都属于同一站点的话,那么 B 页面复用 A 页面的渲染过程;如果是其余状况,浏览器过程则会为 B 创立一个新的渲染过程。
-
具体地讲,咱们将“同一站点”定义为根域名(例如,baidu.com)加上协定(例如,
https://
或者http://
),还蕴含了该根域名下的所有子域名和不同的端口。https://WWW.baidu.com https://WWW.baidu.com:8080
-
因为网络申请可能要花几百毫秒能力拿到响应,这里还会利用一个优化策略。第二步 UI 线程要求网络线程发送申请后,曾经晓得可能要导航到哪个网站去了。因而在发送网络申请的同时,UI 线程会提前分割或并行启动一个渲染器过程。这样在网络线程收到数据后,就曾经有渲染器过程原地待命了。如果产生了重定向,这个待命过程可能用不上,而是换作其余过程去解决。
第五步:提交导航。
数据和渲染器过程都有了,就能够通过 IPC 从浏览器过程向渲染器过程提交导航。渲染器过程也会同时接管到不间断的 HTML 数据流。
当浏览器过程收到渲染器过程的确认音讯后,导航实现,文档加载阶段开始。
此时,地址栏会更新,平安批示图标和网站设置 UI 也会反映新页面的信息。
以后标签页面的会话历史会更新,后退 / 后退按钮起作用。为便于标签页 / 会话在敞开标签页或窗口后复原,会话历史会写入磁盘。
最初一步:初始加载实现。
提交导航之后,渲染器过程将负责加载资源和渲染页面(具体细节前面介绍)。
而在“实现”渲染后(在所有 iframe 中的 onload
事件触发且执行实现后),渲染器过程会通过 IPC 给浏览器过程发送一个音讯。此时,UI 线程进行标签页上的旋转图标。
初始加载实现后,客户端 JavaScript 依然可能加载额定资源并从新渲染页面。
如果此时用户在地址又输出了其余 URL 呢?浏览器过程还会反复上述步骤,导航到新站点。不过在此之前,须要确认已渲染的网站是否关注 beforeunload
事件。因为标签页中的所有,包含 JavaScript 代码都由渲染器过程解决,所以浏览器过程必须与以后的渲染器过程确认后再导航到新站点。
如果导航申请来自以后渲染器过程(用户点击了链接或 JavaScript 运行了 window.location = "https://newsite.com"
),渲染器过程首先会查看beforeunload
处理程序。而后,它会走一遍与浏览器过程触发导航同样的过程。惟一的区别在于导航申请是由渲染器过程提交给浏览器过程的。
导航到不同的网站时,会有一个新的独立渲染器过程负责解决新导航,而老的渲染器过程要负责解决 unload
之类的事件。
更多细节,能够参考【译】页面生命周期 API 以及 Web 页面生命周期。
另外,导航阶段还可能波及【中】Service Worker,即网页利用中的网络代理服务,开发者能够通过它管制什么缓存在本地,何时从网络获取新数据。
Service Worker 说到底也是须要渲染器过程运行的 JavaScript 代码。如果网站注册了 Server Worker,那么导航申请到来时,网络线程会依据 URL 将其匹配进去,此时 UI 线程就会分割一个渲染器过程来执行 Service Worker 的代码:可能只有从本地缓存读取数据,也可能须要发送网络申请。
如果 Service Worker 最终决定从网络申请数据,浏览器过程与渲染器过程间的这种往返通信会导致提早。
因而,这里会有一个“导航预加载”的优化 Speed up Service Worker with Navigation Preloads,即在 Service Worker 启动同时事后加载资源,加载申请通过 HTTP 头部与服务器沟通,服务器决定是否齐全更新内容。
渲染
渲染是渲染器过程外部的工作,波及 Web 性能的诸多方面(具体内容能够参考这里 Why does speed matter?)。
标签页中的所有都由渲染器过程负责解决,其中主线程负责运行大多数客户端 JavaScript 代码,大量代码可能会由工作线程解决(如果用到了 Web Worker 或 Service Worker)。
合成器(compositor)线程和栅格化(raster)线程负责高效、平滑地渲染页面。
渲染器过程的外围工作是把 HTML、CSS 和 JavaScript 转换成用户能够交互的网页接下来,咱们从整体上过一遍渲染器过程解决 Web 内容的各个阶段。
解析 HTML
构建 DOM
渲染器过程收到导航的提交音讯后,开始接管 HTML,其主线程开始解析文本字符串(HTML),并将它转换为 DOM(Document Object Model,文档对象模型)。
DOM 是浏览器外部对页面的示意,也是 JavaScript 与之交互的数据结构和 API。
如何将 HTML 解析为 DOM 由 HTML 规范定义。HTML 规范要求浏览器兼容谬误的 HTML 写法,因而浏览器会“饮泣吞声”,绝不报错。详情能够看看“解析器错误处理及怪异情景简介”。
加载子资源
网站都会用到图片、CSS 和 JavaScript 等内部资源。浏览器须要从缓存或网络加载这些文件。主线程能够在解析并构建 DOM 的过程中发现一个加载一个,但这样效率太低。
为此,Chrome 会在解析同时并发运行“预加载扫描器”,当发现 HTML 文档中有 <img>
或<link>
时,预加载扫描器会将申请提交给浏览器过程中的网络线程。
JavaScript 可能阻塞解析
如果 HTML 解析器碰到 <script>
标签,会暂停解析 HTML 文档并加载、解析和执行 JavaScript 代码。
因为 JavaScript 有可能通过 document.write()
批改文档,进而扭转 DOM 构造(HTML 规范的“解析模型”有一张图能够高深莫测)。所以 HTML 解析器必须停下来执行 JavaScript,而后再复原解析 HTML。至于执行 JavaScript 的细节,大家能够关注 V8 团队相干的分享:【译】JavaScript 引擎根底:Shapes 和 Inline Caches。
提醒浏览器你要加载资源
为了更好地加载资源,能够通过很多形式通知浏览器。如果 JavaScript 没有用到
document.write()
,能够在<script>
标签上增加async
或defer
属性。这样浏览器就会异步运行 JavaScript 代码,不会阻塞解析。适合的话,能够思考应用 JavaScript modules。再比方,<link rel="preload">
通知浏览器该资源对于以后导航相对必要,应该尽快下载。对于资源加载优先级,能够参考这里:【译】Fast load times。
计算款式(Recalculate style)
光有 DOM 还不行,因为并不知道页面应该长啥样。所以接下来,主线程要解析 CSS 并计算每个 DOM 节点的款式。这个过程就是依据 CSS 选择符,确定每个元素要利用什么款式。在 Chrome 开发工具“计算的款式”(computed)中能够看每个元素计算后的款式。
1.把 CSS 转换为浏览器可能了解的构造
CSS 款式起源次要有三种:
- 通过
link
援用的内部 CSS 文件; - <style> 标记内的 CSS;
- 元素的
style
属性内嵌的 CSS。 - 和 HTML 文件一样,浏览器也是无奈间接了解这些纯文本的 CSS 款式,所以当渲染引擎接管到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器能够了解的构造——
styleSheets
。 - 为了加深了解,你能够在 Chrome 控制台中查看其构造,只须要在控制台中输出
document.styleSheets
,而后就看到如下图所示的构造,该数据结构同时具备了查问和批改性能。
2.转换样式表中的属性值,使其标准化
3.计算出 DOM 树中每个节点的具体款式
这就波及到 CSS 的继承规定和层叠规定了。
首先是 CSS 继承。
- 首先,能够抉择要查看的元素的款式(位于图中的区域 2 中),在图中的第 1 个区域中点击对应的元素元素,就能够了上面的区域查看该元素的款式了。比方这里咱们抉择的元素是
<p>
标签,位于html.body.div.
这个门路上面。 - 其次,能够从款式起源(位于图中的区域 3 中)中查看款式的具体起源信息,看看是来源于款式文件,还是来源于 UserAgent 样式表。这里须要特地提下 UserAgent 款式,它是浏览器提供的一组默认款式,如果你不提供任何款式,默认应用的就是 UserAgent 款式。
- 最初,能够通过区域 2 和区域 3 来查看款式继承的具体过程。
款式计算过程中的第二个规定是款式层叠。层叠是 CSS 的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。
布局(Layout)
到这一步,渲染器过程晓得了文档的构造,也晓得了每个节点的款式。但基于这些信息依然不足以渲染页面。
布局,就是要找到元素间的几何地位关系。主线程会遍历 DOM 元素及其计算款式,而后结构一棵布局树,这棵树的每个节点将带有坐标和大小信息。
布局树与 DOM 树的构造相似,但只蕴含页面中可见元素的信息。如果元素被利用了 display: none
,则布局树中不会蕴含它(visibility: hidden
的元素会蕴含在内)。相似地,通过伪类 p::before{content: 'Hi!'}
增加的内容会蕴含在布局树中,但 DOM 树中却没有。
为了构建布局树,浏览器大体上实现了上面这些工作:
- 遍历 DOM 树中的所有可见节点,并把这些节点加到布局中;
- 而不可见的节点会被布局树疏忽掉,如
head
标签上面的全部内容,再比方body.p.span
这个元素,因为它的属性蕴含dispaly:none
,所以这个元素也没有被包进布局树。
确定页面的布局要思考很多很多因素,并不简略。比方,字体大小、文本换行都会影响段落的形态,进而影响后续段落的布局。CSS 可让元素浮动到一边、暗藏溢出边界的内容、扭转文本显示方向。可想而知,布局阶段的工作是十分艰巨的。Chrome 有一个工程师团队专司布局,感兴起的话,能够看看他们这个分享:BlinkOn 8: Block Layout Deep Dive(在 YouTube 上)。
更新了元素的几何属性(重排)
从上图能够看出,如果你通过 JavaScript 或者 CSS 批改元素的几何地位属性,例如扭转元素的宽度、高度等,那么浏览器会触发从新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排须要更新残缺的渲染流水线,所以开销也是最大的
分层(Layer)
因为页面中有很多简单的成果,如一些简单的 3D 变换、页面滚动,或者应用 z -indexing 做 z 轴排序等,为了更加不便地实现这些成果,渲染引擎还须要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。
要想直观地了解什么是图层,你能够关上 Chrome 的“开发者工具”,抉择“Layers”标签(开发者工具 -> More tools ->Layers),就能够可视化页面的分层状况。
通常状况下,并不是布局树的每个节点都蕴含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。
那么须要满足什么条件,渲染引擎才会为特定的节点创立新的层呢?
第一点,领有层叠上下文属性的元素会被晋升为独自的一层。
从图中能够看出,明确定位属性的元素、定义通明属性的元素、应用 CSS 滤镜的元素等,都领有层叠上下文属性。
第二点,须要剪裁(clip)的中央也会被创立为图层。
<style>
div {
width: 200;
height: 200;
overflow:auto;
background: gray;
}
</style>
<body>
<div >
<p> 所以元素有了层叠上下文的属性或者须要被剪裁,那么就会被晋升成为独自一层,你能够参看下图:</p>
<p> 从上图咱们能够看到,document 层上有 A 和 B 层,而 B 层之上又有两个图层。这些图层组织在一起也是一颗树状构造。</p>
<p> 图层树是基于布局树来创立的,为了找出哪些元素须要在哪些层中,渲染引擎会遍历布局树来创立层树(Update LayerTree)。</p>
</div>
</body>
在这里咱们把 div 的大小限定为 200 200 像素,而 div 外面的文字内容比拟多,文字所显示的区域必定会超出 200 200 的面积,这时候就产生了剪裁。
呈现这种裁剪状况的时候,渲染引擎会为文字局部独自创立一个层,如果呈现滚动条,滚动条也会被晋升为独自的层。
如果页面某些局部应该独立一层(如滑入的菜单)但却没有,那你能够在 CSS 中给它加上 will-change
属性来揭示浏览器。
分层并不是越多越好,合成过多的层有可能还不如每帧都对页面中的一小部分执行一次栅格化更快。对于这里边的衡量,能够参考:保持仅合成器的属性和管理层计数。
图层绘制(Paint)
在实现图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制,那么接下来咱们看看渲染引擎是怎么实现图层绘制的?
渲染引擎实现图层的绘制,会把一个图层的绘制拆分成很多小的绘制指令,而后再把这些指令依照程序组成一个待绘制列表,如下图所示:
从图中能够看出,绘制列表中的指令其实非常简单,就是让其执行一个简略的绘制操作,比方绘制粉色矩形或者彩色的线等。
而绘制一个元素通常须要好几条绘制指令,因为每个元素的背景、前景、边框都须要独自的指令去绘制。所以在图层绘制阶段,输入的内容就是这些 待绘制列表。
更新元素的绘制属性(重绘)
从图中能够看出,如果批改了元素的背景色彩,那么布局阶段将不会被执行,因为并没有引起几何地位的变换,所以就间接进入了绘制阶段,而后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。
栅格化(raster)
绘制列表只是用来记录绘制程序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来实现的。
当图层的绘制列表筹备好之后,主线程会把该绘制列表提交(commit)给 合成线程。
- 通常一个页面可能很大,然而用户只能看到其中的一部分,咱们把用户能够看到的这个局部叫做视口(viewport)。
-
有些状况下,图层很大,然而通过视口,用户只能看到页面的很小一部分。所以这种状况下,要绘制出所有图层内容的话,没有必要。
基于这个起因,合成线程会将图层划分为图块(tile),这些图块的大小通常都是 256 x 256 或者 512 x 512。
合成器线程会安顿栅格化线程优先转换视口(及左近)的图块。而形成一层的图块也会转换为不同分辨率的版本,以便在用户缩放时应用。
所谓 栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染过程保护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的。
通常,栅格化过程都会应用 GPU 来减速生成,应用 GPU 生成位图的过程叫疾速栅格化,或者 GPU 栅格化,生成的位图被保留在 GPU 内存中。
置信你还记得,GPU 操作是运行在 GPU 过程中,如果栅格化操作应用了 GPU,那么最终生成位图的操作是在 GPU 中实现的,这就波及到了跨过程操作。
合成
什么是合成?合成(composite)是将页面不同局部先分层并别离栅格化,而后再通过独立的合成器线程合成页面。
这样当用户滚动页面时,因为层都曾经栅格化,所以浏览器惟一要做的就是合成一个新的帧。而动画也能够用同样的形式实现:先挪动层,再合成帧。
所有小片都栅格化当前,合成器线程会收集叫做“绘制方块”(draw quad)的图块信息,以创立合成器帧。
- 绘制方块:蕴含小片的内存地址、页面地位等合成页面相干的信息。
- 合成器帧:由从多绘制方块拼成的页面中的一帧。
创立好的合成器帧会通过 IPC 提交给浏览器过程。浏览器过程外面有一个叫 viz
的组件,用来接管合成线程发过来的 DrawQuad
命令,而后依据 DrawQuad
命令,将其页面内容绘制到内存中,最初再将内存显示在屏幕上。
与此同时,为更新浏览器界面,UI 线程可能还会增加另一个合成器帧;或者因为有扩大,其余渲染器过程也可能增加额定的合成器帧。
所有这些合成器帧都会发送给 GPU,以便最终显示在屏幕上。如果产生滚动事件,合成器线程会再创立新的合成器帧并发送给 GPU。
应用合成的益处是不必牵扯主线程。合成器线程不必期待款式计算或 JavaScript 执行。
这也是为什么“只需合成的动画”(【中】High Performance Animations)被认为性能最佳的起因。因为如果布局和绘制须要再次计算,那还得用到主线程。
用一张图来展现:
渲染流水线大总结
好了,咱们当初曾经剖析完了整个渲染流程,从 HTML 到 DOM、款式计算、布局、图层、绘制、光栅化、合成和显示。上面我用一张图来总结下这整个渲染流程:
- 渲染过程将 HTML 内容转换为可能读懂的 DOM 树结构。
- 渲染引擎将 CSS 样式表转化为浏览器能够了解的 styleSheets,计算出 DOM 节点的款式。
- 创立布局树,并计算元素的布局信息。
- 对布局树进行分层,并生成分层树。
- 为每个图层生成绘制列表,并将其提交到合成线程。
- 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
- 合成线程发送绘制图块命令 DrawQuad 给浏览器过程。
- 浏览器过程依据 DrawQuad 音讯生成页面,并显示到显示器上。
参考:
深刻了解古代浏览器
浏览器工作原理与实际
[[译] 古代浏览器外部揭秘(第一局部)](https://juejin.cn/post/684490…