chrome浏览器的多进程架构
前端终极提问:输入URL到页面展示,发生了什么(1)--导航
页面的渲染机制是非常复杂,从输入HTML、CSS、JavaScript 等文件,到浏览器就会显示出完整的页面,是要经过多个复杂的过程的。一般来说要经过以下几个阶段 建 DOM 树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。
构建 DOM 树
浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树。
![0812f2093cc969547062b540b67b4390.png](https://i-blog.csdnimg.cn/blog_migrate/f4c4db4bf6cb05deeaf281112c094006.jpeg)
样式计算(Recalculate Style)
样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式。
这个阶段可分为三步来完成。
1、将 CSS 文本转换为 styleSheets
和 HTML 文件一样,浏览器也是无法直接理解这些纯文本的 CSS 样式,所以当向渲染引擎接输入 CSS 文本后,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets。
CSS 样式来源主要有三种:
- 通过 link 引用的外部 CSS 文件
<style>
标记内的 CSS- 元素的 style 属性内嵌的 CSS
在 Chrome 控制台中输入 document.styleSheets,然后就看到document.styleSheets的结构
![fb448eb0fc3a1b92d866155a29eb990b.png](https://i-blog.csdnimg.cn/blog_migrate/e497e2884957177276a6119ba17abdb1.jpeg)
这个样式表包含了各种来源的css文件生成的styleSheets。
2、标准化styleSheets中的属性值
body { font-size: 2em }
p {color:blue;}
span {display: none}
div {font-weight: bold}
div p {color:green;}
div {color:red; }
上面的 CSS 文本中有很多属性值,如 2em、red、bold,这些类型数值不容易被渲染引擎理解,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值。
![c4b458d1e42bab8da1b35860bb4b237a.png](https://i-blog.csdnimg.cn/blog_migrate/d00c54bbc1d39d59ed230dd87c5484cf.jpeg)
3、计算出 DOM 树中每个节点的具体样式
这涉及 CSS 的继承和层叠了。
继承就是每个 DOM 节点都包含有父节点的可继承样式(如 font-size , color 是可以继承的)。
层叠 定义了如何合并来自多个源的样式的算法,也就是css样式的优先级。(这个可以参考网上的其他文章)
CSS 7 种基础的选择器:
ID 选择器, 如 #id{}
类选择器, 如 .class{}
属性选择器, 如 a[href="segmentfault.com"]{}
伪类选择器, 如 :hover{}
伪元素选择器, 如 ::before{}
标签选择器, 如 span{}
通配选择器, 如 *{}
![b2ad8cf014777418b633319c968e6612.png](https://i-blog.csdnimg.cn/blog_migrate/c00c0d3b0f0588a123a4f405f5038acc.jpeg)
通过上图可以很清晰的看到:元素的样式的继承过程(区域2) 和 样式的来源 (区域3)。
DOM 元素最终的计算样式,可以打开 Chrome 的“开发者工具” 查看。
![8f5e580e7c2d308265d9a9e40d825ad9.png](https://i-blog.csdnimg.cn/blog_migrate/8727771fc80c219533009816719004c5.jpeg)
布局阶段
现在有 DOM 树和 DOM 树中元素的样式,那么接下来就需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局。
布局可以分为两步:创建布局树和布局计算。
创建布局树
DOM 树还含有很多不可见的元素,比如 head 标签,还有使用了 display:none 属性的元素。所以在显示之前,我们还要额外地构建一棵只包含可见元素布局树。
![c01bd93642458d4ceba4510fca1c0098.png](https://i-blog.csdnimg.cn/blog_migrate/be3b2e7b0ce8c1282a254fc16ac35f7b.jpeg)
生成的布局树中只包含可见的节点。
布局计算
布局计算,就是计算布局树中每个元素的几何坐标位置,并把计算结果保存在布局树中。
分层
页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。这类似于PS中的图层概念,正是这些图层叠加在一起构成了最终的页面图像。
![6fe9401e0f4c285316264fd5c9312b20.png](https://i-blog.csdnimg.cn/blog_migrate/e312569e150701768fb533a571897e7f.jpeg)
通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。如上图中的 span 标签没有专属图层,那么它们就从属于它们的父节点图层。
通常满足下面两点中任意一点的元素,渲染引擎才会为它创建新的层。
1、拥有层叠上下文属性的元素会被提升为单独的一层
![5edd7e50f3309e4f94ef1e8730780852.png](https://i-blog.csdnimg.cn/blog_migrate/fc5cffac9a77abbcef24b6e8a9df39d5.png)
层叠上下文由满足以下任意一个条件的元素形成:
- 文档根元素(
<html>
); position
值为absolute
(绝对定位)或relative
(相对定位)且z-index
值不为auto
的元素;position
值为fixed
(固定定位)或sticky
(粘滞定位)的元素(沾滞定位适配所有移动设备上的浏览器,但老的桌面浏览器不支持);- flex (
flexbox
) 容器的子元素,且z-index
值不为auto
; - grid (
grid
) 容器的子元素,且z-index
值不为auto
; opacity
属性值小于1
的元素;mix-blend-mode
属性值不为normal
的元素;- 以下任意属性值不为
none
的元素:transform
filter
perspective
clip-path
mask
/mask-image
/mask-border
isolation
属性值为isolate
的元素;-webkit-overflow-scrolling
属性值为touch
的元素;will-change
值设定了任一属性而该属性在 non-initial 值时会创建层叠上下文的元素(参考这篇文章);contain
属性值为layout
、paint
或包含它们其中之一的合成值(比如contain: strict
、contain: content
)的元素。
在层叠上下文中,子元素同样也按照上面解释的规则进行层叠。 重要的是,其子级层叠上下文的 z-index
值只在父级中才有意义。子级层叠上下文被自动视为父级层叠上下文的一个独立单元。
2、需要剪裁(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 的面积,这时候就产生了剪裁,渲染引擎会把裁剪文字内容的一部分用于显示在 div 区域,下图是运行时的分层结果:
![877834b573fc3d71d69a3e289ca3af92.png](https://i-blog.csdnimg.cn/blog_migrate/96825c648e71661b2de5b9bdfa24f4ae.jpeg)
图层绘制
在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制,渲染引擎会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表,如下图所示:
![e1dbbd9700f4083bd9d3080ae67582c8.png](https://i-blog.csdnimg.cn/blog_migrate/70b927ef5f97a0f069c54969f74e1ce5.jpeg)
绘制列表中的指令其实非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。所以在图层绘制阶段,输出的内容就是这些待绘制列表。
打开“开发者工具”的“Layers”标签,选择“document”层,就可以看到绘制列表。
![c41cbb283b2694cc4d94be81dfc1d672.png](https://i-blog.csdnimg.cn/blog_migrate/a840693d5c49cd70fec8ada12fa9a59f.jpeg)
栅格化(raster)操作
图层绘制这个过程输出的是绘制列表,绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。你可以结合下图来看下渲染主线程和合成线程之间的关系:
![8f7d6c229ab82d7c50238ff2e8ef0e23.png](https://i-blog.csdnimg.cn/blog_migrate/78164acff94748fb6b2e517a82a4efa8.jpeg)
当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程。
在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。
通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做 视口(viewport)。
![fdc282d26926c664dd99df0e96b5d26f.png](https://i-blog.csdnimg.cn/blog_migrate/3d40a602a5a56640281a0825ff912dbd.jpeg)
基于这个原因,合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512,如下图所示:
![48aeeee929f21d279a018284144399fd.png](https://i-blog.csdnimg.cn/blog_migrate/4b7a5ffaf918f4fc2ebd6f5514266eaa.jpeg)
然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,运行方式如下图所示:
![85e353cd27333b77a40709093b67c677.png](https://i-blog.csdnimg.cn/blog_migrate/f4c6f29e252ac6a4ca8c9fb4b32a0580.jpeg)
通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。
相信你还记得,GPU 操作是运行在 GPU 进程中,如果栅格化操作使用了 GPU,那么最终生成位图的操作是在 GPU 中完成的,这就涉及到了跨进程操作。具体形式你可以参考下图:
![f4094c1daf4427601305ce5687845e8e.png](https://i-blog.csdnimg.cn/blog_migrate/bca8dede0b23a2eee2c89998461277f0.jpeg)
渲染进程把生成图块的指令发送给 GPU,然后在 GPU 中执行生成图块的位图,并保存在 GPU 的内存中。
合成和显示
一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
到这里,经过这一系列的阶段,编写好的 HTML、CSS、JavaScript 等文件,经过浏览器就会显示出漂亮的页面了。
渲染流水线大总结
好了,我们现在已经分析完了整个渲染流程,从 HTML 到 DOM、样式计算、布局、图层、绘制、栅格化、合成和显示。下面我用一张图来总结下这整个渲染流程:
![6e3929850676ecec536bd0ae0f7f73a2.png](https://i-blog.csdnimg.cn/blog_migrate/127cd364ddc66bbf2dd3db6ffec28dac.jpeg)
一个完整的渲染流程大致可总结为如下:
- 渲染进程将 HTML 内容转换为能够读懂的DOM 树结构。(生成dom树)
- 渲染引擎将 CSS 样式表转化为浏览器可以理解的styleSheets,计算出 DOM 节点的样式。(样式计算)
- 创建布局树,并计算元素的布局信息。(布局)
- 对布局树进行分层,并生成分层树。(分层)
- 为每个图层生成绘制列表,并将其提交到合成线程。(图层绘制)
- 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。(栅格化)
- 合成线程发送绘制图块命令DrawQuad给浏览器进程。 (合成和显示)
- 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。 (合成和显示)
在了解了渲染流程后,我们再来看看三个和渲染相关的概念——“重排”“重绘”和“合成”。
重排
![45eb00e68d79fc59922d2535f2aa1929.png](https://i-blog.csdnimg.cn/blog_migrate/32a1fe5e31390363cf0a6c1042a383dc.jpeg)
通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。
重绘
![4091d8c9e3367d7f6a79b204df3e5982.png](https://i-blog.csdnimg.cn/blog_migrate/954c4394282a53364cbe539bd2b60d0a.jpeg)
如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。
3. 直接合成阶段
那如果你更改一个既不要布局也不要绘制的属性,会发生什么变化呢?渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成。具体流程参考下图:
![00bbb6211157e2d70f373d219a6144a4.png](https://i-blog.csdnimg.cn/blog_migrate/8ee9991e035970b46ad010c0f11d819e.jpeg)
在上图中,我们使用了 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。
摘抄自:HTML、CSS和JavaScript,是如何变成页面的?