前言
这个阶段很重要,了解其相关流程能让你“看透”页面是如何工作的,有了这些知识,你可以解决一系列相关的问题,比如能熟练使用开发者工具,因为能够理解开发者工具里面大部分项目的含义,能优化页面卡顿问题,使用 JavaScript 优化动画流程,通过优化样式表来防止强制同步布局,等等。
渲染流水线
由于渲染机制过于复杂,所以渲染模块在执行过程中会被划分为很多子阶段,输入的 HTML 经过这些子阶段,最后输出像素。我们把这样的一个处理流程叫做渲染流水线。
大致流程是这样的:
按照渲染的时间顺序,流水线可分为如下几个子阶段:
- 构建 DOM 树
- 样式计算
- 布局
- 分层
- 绘制
- 分块
- 光栅化
- 合成
在学习过程中,需关注以下三点内容:
- 各个子阶段的输入内容
- 各个子阶段的处理过程
- 各个子阶段的输出内容
构建 DOM 树
为什么要构建 DOM 树?
因为浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构 —— DOM 树。
DOM 树的构建过程:
样式计算
样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式,这个阶段大体可分为三步来完成。
把 CSS 转换为浏览器能够理解的结构
CSS 样式来源主要有三种:
-
通过 link 引用的外部 CSS 文件
-
<style>标记内的 CSS -
元素的 style 属性内嵌的 CSS
当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构 —— StyleSheets。
转换样式表中的属性值,使其标准化
如 2em、blue、bold,这些类型数值不容易被渲染引擎理解, 所以要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。
计算出 DOM 树中每个节点的具体样式
现在样式的属性已被标准化了,接下来就需要计算 DOM 树中每个节点的样式属性了,如何计算呢?
这就涉及到 CSS 的继承规则和层叠规则了。
-
CSS 继承。
CSS 继承就是每个 DOM 节点都包含有父节点的样式。
body { font-size: 20px } p {color:blue;} span {display: none} div {font-weight: bold;color:red} div p {color:green;}这张样式表最终应用到 DOM 节点的效果如下图所示:
这里需要特别提下 UserAgent 样式, 它是浏览器提供的一组默认样式,如果你不提供任何样式,默认使用的就是 UserAgent 样式。
-
样式层叠
层叠是 CSS 的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。它在 CSS 处于核心地位,CSS 的全称 “层叠样式表” 正是强调了这一点。
样式层叠
布局阶段
现在,我们有 DOM 树和 DOM 树中元素的样式,但这还不足以显示页面,因为我们还不 知道 DOM 元素的几何位置信息。
那么接下来就需要计算出 DOM 树中可见元素的几何位 置,我们把这个计算过程叫做布局。
Chrome 在布局阶段需要完成两个任务:创建布局树和布局计算。
创建布局树
你可能注意到了 DOM 树还含有很多不可见的元素,比如 head 标签,还有使用了 display:none 属性的元素。
所以在显示之前,我们还要额外地构建一棵只包含可见元素布局树。
从上图可以看出,DOM 树中所有不可见的节点都没有包含到布局树中。
为了构建布局树,浏览器大体上完成了下面这些工作:
-
遍历 DOM 树中的所有可见节点,并把这些节点加到布局中
-
不可见的节点会被布局树忽略掉
如 head 标签下面的全部内容,再比如 body.p.span 这个元素,因为它的属性包含 dispaly:none,所以这个元素也没有被包进布局树
布局计算
现在我们有了一棵完整的布局树。那么接下来,就要计算布局树节点的坐标位置了。
在执行布局操作的时候,会把布局运算的结果重新写回布局树中,所以布局树既是输入内容 也是输出内容,这是布局阶段一个不合理的地方,因为在布局阶段并没有清晰地将输入内容 和输出内容区分开来。针对这个问题,Chrome 团队正在重构布局代码,下一代布局系统 叫 LayoutNG,试图更清晰地分离输入和输出,从而让新设计的布局算法更加简单。
分层
现在我们有了布局树,而且每个元素的具体位置信息都计算出来了,那么接下来是不是就要开始着手绘制页面了?
答案依然是否定的。
因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树。如果你熟悉 PS,相信你会很容易理解图层的概念,正是这些图层叠加在一起构成了最终的页面图像。
Chrome devtools layer
从上图可以看出,渲染引擎给页面分了很多图层,这些图层按照一定顺序叠加在一起,就形成了最终的页面。
浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。
下面我们再来看看这些图层和布局树节点之间的关系:
并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么 这个节点就从属于父节点的图层。
如上图中的 span 标签没有专属图层,那么它们就从属于它们的父节点图层。但不管怎样,最终每一个节点都会直接或者间接地从属于一个层。
需要满足什么条件,渲染引擎才会为特定的节点创建新的层呢?通常满足下面两点中任意一点的元素就可以被提升为单独的一个图层。
-
拥有层叠上下文属性的元素会被提升为单独的一层。
-
需要剪裁(clip)的地方也会被创建为图层。
通俗的说,需要滚动条的情况
图层绘制
在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制。
那具体要怎么绘制呢?
试想一下,如果给你一张纸,让你先把纸的背景涂成蓝色,然后在中间位置画一个红色的圆,最后再在圆上画个绿色三角形。你会怎么操作呢?
通常,你会把你的绘制操作分解为三步:
- 绘制蓝色背景;
- 在中间绘制一个红色的圆;
- 再在圆上绘制绿色三角形。
渲染引擎实现图层的绘制与之类似,会把一个图层的绘制拆分成很多小的绘制指令,然后再 把这些指令按照顺序组成一个待绘制列表。
注意:这一步没有绘制,只是生成了绘制列表。
栅格化(raster)操作
绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。
当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程。
合成线程会将图层划分为图块。
合成线程会按照视口附近的图块来优先生成位图(一次绘制出所有图层内容开销太大),实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。
图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的。
通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化, 或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。
合成和显示
一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”, 然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
总结
渲染进程会做这些任务:
渲染流程:
每个阶段的任务:
-
DOM 生成(DOM)
在 HTML 页面内容被提交给渲染引擎之后,渲染引擎首先将 HTML 解析为浏览器可以理解的 DOM
渲染进程主线程
-
样式计算(Style)
渲染引擎将 CSS 样式表转化为浏览器可以理解的styleSheets,计算出 DOM 节点的样式。
渲染进程主线程
-
布局(Layout)
计算每个元素的几何坐标位置,并将这些信息保存在布局树中
渲染进程主线程
-
分层(Layer)
对布局树进行分层,并生成分层树(为渲染树的节点生成图层)
渲染进程主线程
-
绘制(Paint)
把图层的绘制分成小的绘制指令,生成记录绘制顺序和绘制指令的列表
渲染进程主线程
-
分块(tailes)
将图层划分为图块
渲染进程合成线程
-
光栅化(raster)
将图块转换为位图
光栅化线程(这里存在光栅化线程池)
光栅化线程会调用 GPU 进程
-
合成(draw quad)
合成线程发送绘制图块命令DrawQuad给浏览器进程。
浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。
一个完整的渲染流程大致可总结为如下:
- 渲染进程将 HTML 内容转换为能够读懂的DOM 树结构。
- 渲染引擎将 CSS 样式表转化为浏览器可以理解的styleSheets,计算出 DOM 节点的样式。
- 创建布局树,并计算元素的布局信息。
- 对布局树进行分层,并生成分层树。
- 为每个图层生成绘制列表,并将其提交到合成线程。
- 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
- 合成线程发送绘制图块命令DrawQuad给浏览器进程。
- 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。
回流(重排)、重绘