渲染流程
HTML, CSS, JavaScript是如何变成页面的?
由于渲染过程过于复杂,所以渲染模块在执行过程中会被划分为很多子阶段,输入的HTML,CSS,JavaScript经过这些子阶段,最后输出像素。这样的一个处理流程称为渲染流水线。
按照渲染的时间顺序划分,可将渲染流水线划分为:
- 构建DOM树(DOM)
- 样式计算(Style)
- 布局阶段(Layout)
- 分层(Layer)
- 绘制(Paint)
- 分块(tiles)
- 光栅化(raster)
- 合成和显示(display)
一、构建DOM树(DOM)
浏览器是无法直接理解和使用HTML的,所以需要将HTML转换为浏览器能理解的结构——DOM树。
1. Input
HTML文件
2. Handle
HTML解析器解析
3. Output
DOM树(DOM树是保存在内存中的树状结构,document)
二、样式计算(Recalculate Style)
同样的,浏览器是无法理解纯文本的CSS样式的,所以渲染引擎会把CSS转化为可以理解的styleSheets,并计算出DOM节点中每个元素的具体样式。
计算的目的是为了计算出DOM节点中每个元素的具体样式。
1. Input
<link ref="stylesheet" href="">
<style></style>
style=""
2. Handle
-
把CSS转换为浏览器能够理解的结构
浏览器会把CSS转换为浏览器能够理解的结构——styleSheets(document.styleSheets)
注:也有人将styleSheet称之为CSSOM。
-
转换样式表中的属性值,使其标准化
将所有属性值转为渲染引擎容易理解的、标准化的计算值。如颜色值会转为rgb或rgba形式
-
计算出DOM树中每个节点的具体样式:
根据CSS的继承规则和层叠规则计算出DOM树中每个节点的样式属性。
3. Output
每个DOM节点的样式,保存在ComputedStyle中
三、布局阶段(Layout)
计算出DOM树中可⻅元素的⼏何位置信息。
Chrome在布局阶段需要完成两个任务: 创建布局树和布局计算。
1. 创建布局树
-
Input:DOM树,ComputedStyle
-
Handle:
遍历DOM树中的所有可⻅节点, 并把这些节点加到布局树中,⽽不可⻅的节点(如head标签的节点,display:none)会被布局树忽略掉
-
Output:⼀棵只包含可⻅元素布局树(LayoutTree)
2. 布局计算
-
Input:布局树(LayoutTree)
-
Handle:计算布局树节点的坐标位置,并重新写回布局树
-
Output:布局树(LayoutTree)
布局阶段有⼀个不合理的地⽅:在执⾏布局操作的时候, 会把布局计算的结果重新写回布局树中, 所以布局树既是输⼊内容也是输出内容,并没有清晰地将输⼊内容和输出内容区分开来。针对这个问题, Chrome团队正在重构布局代码, 下⼀代布局系统叫LayoutNG, 试图更清晰地分离输⼊和输出, 从⽽让新设计的布局算法更加简单。
注:也有人将LayoutTree称之为RenderTree,但两者之间还是有些差别的。
四、分层(Layer)
⻚⾯中有很多复杂的效果,如⼀些复杂的3D变换、⻚⾯滚动,或者使⽤z-index做z轴排序等。
为了更加⽅便地实现这些效果,渲染引擎还需要为特定的节点⽣成专⽤的图层,并⽣成⼀棵对应的图层树(LayerTree)
Chrome的“开发者⼯具”,选择“Layers”标签,就可以可视化⻚⾯的分层情况。
1. Input
布局树(LayoutTree)
2. Handle
为特定的节点生成专用的图层。
并不是布局树的每个节点都是⼀个图层的,如果⼀个节点没有对应的层,那么这个节点就从属于⽗节点的图层。最终每⼀个节点都会直接或者间接地从属于某⼀个层。
当满足以下任意一条特性时,就会为该节点生成专用的图层:
-
拥有层叠上下⽂属性的元素会被提升为单独的⼀层。如position的fixed,absolute以及z-index等。
-
需要剪裁(clip)的地⽅也会被创建为图层,如文字内容超出了限定区域就会创建图层,同时滚动条也会创建一个图层。
1
2
3
4<div style="width: 200px;height: 200px;overflow: auto;background: gray;">
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Asperiores quia nemo assumenda, repellendus voluptatibus suscipit recusandae corporis odit facilis atque porro aliquid, ea iste molestiae magnam unde, ut nostrum molestias!
</p>
</div>
3. Output
一颗对应的图层树(LayerTree)
五、绘制(Paint)
渲染引擎会对图层树(LayerTree)中的每个图层⽣成绘制列表,并将其提交到合成线程。
绘制列表只是⽤来记录绘制顺序和绘制指令的列表,⽽实际上绘制操作是由渲染引擎中的合成线程来完成的。
同样的,Chrome的“开发者⼯具”,选择“Layers”标签,可以查看绘制指令。
1. Input
图层树(LayerTree)
2. Handle
- 把一个图层的绘制拆成很多小的绘制指令
- 将这些绘制指令按顺序组成一个待绘制列表
3. Output
生成待绘制列表,并将待绘制列表提交给合成线程。
六、分块(tiles)
当绘制列表准备好之后,渲染进程的主线程会把该绘制列表提交(commit)给渲染进程中的合成线程。
通常一个页面可能很大,但是用户通过屏幕只能看到其中的一小块,我们把通过屏幕可以看到的这个部分叫做视口(物理视口)。
在有些情况下,有的图层很⼤,⽐如有的⻚⾯要滚动好久才能滚动到底部,但是通过视⼝,⽤⼾只能看到⻚⾯的很⼩⼀部分,所以在这种情况下,要绘制出所有图层内容的话,就会产⽣太⼤的开销,⽽且也没有必要。
基于这个原因,合成线程会将图层划分为图块(tile)。通常这些图块的⼤⼩是256x256或者512x512。
1. Input
待绘制列表(一个绘制列表就是一个layer)
2. Handle
将图层划分成图块
3. Output
图层对应的图块
七、光栅化(raster)
所谓栅格化就是指将图块转换为位图。所以栅格化执行的最小单位就是图块。
渲染进程维护了一个栅格化的线程池,所有的图块栅格化操作都是在线程池内执行的。
光栅化就是指在栅格化的过程中,会使用GPU来加速生成位图,所以也可以称之为快速栅格化或GPU栅格化。
1. Input
图层对应的图块(tiles)
2. Handle
- 栅格化线程池内的栅格化线程会按照视口附近的图块优先生成位图
- 栅格化过程都会使⽤GPU来加速⽣成位图,⽣成的位图被保存在GPU内存中。(跨进程操作)
3. Output
图层的图块对应的位图,并保存在GPU内存中。
一旦所有图块都被光栅化,合成线程会生成一个绘制图块的命令(DrawQuad命令),然后将该命令提交给浏览器进程。
八、合成和显示
根据DrawQuad消息⽣成⻚⾯,并显⽰到显⽰器上。
1. Input
合成线程发过来的DrawQuad命令
2. Handle
浏览器进程里面有一个叫viz的组件,用来接收合成线程发过来的DrawQuad命令。
然后根据DrawQuad命令,将⻚⾯内容绘制到内存中,最后再将内存中的页面显⽰在屏幕上。
3. Output
显示的页面
九、相关概念
1. 重排:更新了元素的几何属性
修改了元素的几何位置属性,例如width,height等,那么浏览器会触发重新布局(Layout),然后按照渲染流水线执行后序的阶段,这个过程就是重排。
重排需要更新完整的渲染流⽔线,所以开销也是最⼤的。
为什么重排会更新完成的渲染流水线?
因为修改了元素的几何属性,那么需要重新的样式计算(Recaculate Style),然后重新布局,分层绘制等,也就是重新开始更新渲染流水线。
2. 重绘:更新了元素的绘制属性
修改了元素的绘制属性,如background, color等,那么浏览器会触发重新绘制(Paint),也就是跳过Layout,Layer阶段,直接进入绘制阶段(Paint),然后按照渲染流水线执行后序的阶段,这个过程就是重绘。
重绘省去了布局(Layout)和分层阶段(Layer),所以执⾏效率会⽐重排操作要⾼⼀些。
为什么重绘会省去布局和分层阶段?
因为修改了元素的背景颜色等属性,那么需要重新的样式计算(Recaculte Style),然而元素的几何位置等属性并没有变,同时Layout和layer阶段是依赖于元素的几何位置信息的,所以完全可以跳过Layout和Layer阶段,直接进入Paint阶段,然后执行后序流程。
3. 合成:直接合成
修改了既不要重新布局也不要重新绘制的属性,会跳过Layout,Layer,Paint阶段,直接在渲染进程的非主线程上执行合成的操作,这个过程就是直接合成。
在⾮主线程上合成,并没有占⽤主线程的资源,另外也避开了布局和绘制两个⼦阶段,所以相对于重绘和重排,合成能⼤⼤提升绘制效率。
参考链接
- 李兵《浏览器工作原理与实践》:https://time.geekbang.org/column/intro/100033601