Chrome 关键渲染路径
浏览器页面加载过程中,可以分为以下阶段:
- 网络请求,服务端返回 HTML 内容;
- 浏览器一边解析 HTML,一边进行页面渲染;
- 解析到外部资源,会发起 HTTP 请求获取,加载 Javascript 代码时会暂停页面渲染;
- 根据业务代码加载过程,会分别进入页面开始渲染、渲染完成、用户可交互等阶段;
- 页面交互过程中,会根据业务逻辑进行逻辑运算、页面更新。
从结构上来说,浏览器主要包括了八个子系统:用户界面、浏览器引擎、渲染引擎、网络子系统、JavaScript 解释器、XML 解析器、显示后端、数据持久性子系统。
关键渲染路径

关键渲染路径就是将 HTML, CSS, JavaScript 文件转化成漂亮页面的过程
DOM 生成
为什么要构建 DOM 树呢?这是因为浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树。
浏览器会遵守一套定义完善的步骤来处理 HTML 并构建 DOM:

第一步:转换
浏览器从磁盘或网络读取 HTML 的原始字节,并根据文件的指定编码(例如 UTF-8)将它们转换成字符,
3C 62 6F 64 79 3E 48 65 6C 6C => <html>
<head>... </head>
<body>第二步: Token 化
将字符串转换成 Token,例如:“ <html> ”、“ <body> ”等。Token 中会标识出当前 Token 是“开始标签”或是“结束标签”亦或是“文本”等信息。
<html>
<head>... </head>
<body>↓
StartTag: html 、 StartTag: head 、 .. 、 EndTag: head 、 StartTag: body
其内部实现是通过维护一个栈,遇到 start 压入栈,遇到 end 弹出,节点与节点之间的关系通过标签的闭合辨别,比如两个相同标签的 start 和 end 内的元素都是其子元素。
第三步:生成节点对象并构建 DOM
事实上,构建 DOM 的过程中,不是等所有 Token 都转换完成后再去生成节点对象,而是一边生成 Token 一边消耗 Token 来生成节点对象。换句话说,每个 Token 被生成后,会立刻消耗这个 Token 创建出节点对象,这些对象分别定义他们的属性和规则。
因为 HTML 标记定义的就是不同标签之间的关系,这个关系就像是一个树形结构一样

样式计算
样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式,这个阶段大体可分为三步来完成。
1. 把 CSS 转换为浏览器能够理解的结构
CSS 样式来源主要有三种:
- 通过 link 引用的外部 CSS 文件
<style>标记内的 CSS- 元素的 style 属性内嵌的 CSS
和 HTML 文件一样,浏览器也是无法直接理解这些纯文本的 CSS 样式,所以当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets。
Bytes → characters → tokens → nodes → CSSOM
只需要在控制台中输入 document.styleSheets,然后就看到如下图所示的结构:

2. 转换样式表中的属性值,使其标准化
需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这 个过程就是属性值标准化。

3. 计算出 DOM 树中每个节点的具体样式
CSS 的继承规则
CSS 继承就是每个 DOM 节点都包含有父节点的样式。
CSS 的层叠规则
层叠是 CSS 的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。它在 CSS 处于核心地位,CSS 的全称“层叠样式表”正是强调了这一点。
注意
不完整的 CSS 是无法使用的,因为 CSS 的每个属性都可以改变 CSSOM。所以必须等 CSSOM 构建完毕才能进入到下一个阶段,哪怕 DOM 已经构建完,它也得等 CSSOM,然后才能进入下一个阶段。
所以,CSS 的加载速度与构建 CSSOM 的速度将直接影响首屏渲染速度,因此在默认情况下 CSS 被视为阻塞渲染的资源。
另外,而 JavaScript 引擎在解析 JavaScript 之前,是不知道 JavaScript 是否操纵了 CSSOM 的,所以渲染引擎在遇到 JavaScript 脚本时,不管该脚本是否操纵了 CSSOM,都会执行 CSS 文件下载,解析操作,再执行 JavaScript 脚本。
所以说 JavaScript 脚本是依赖样式表的,这又多了一个阻塞过程。
现在,我们有 DOM 树和 DOM 树中元素的样式,但这还不足以显示页面,因为我们还不知道 DOM 元素的几何位置信息。那么接下来就需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局。
1. 创建渲染树
为了构建渲染树,浏览器大体上完成了下面这些工作:
- 遍历 DOM 树中的所有可见节点,并把这些节点加到布局中;
- 而不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容,再比如 body.p.span 这个元素,因为它的属性包含 dispaly:none,所以这个元素也没有被包进布局树。
2. 布局计算
现在的布局计算 LayerTree
因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。
那么需要满足什么条件,渲染引擎才会为特定的节点创建新的层呢?
第一点,拥有层叠上下文属性的元素会被提升为单独的一层。
- 档根元素(
<html>); position值为absolute(绝对定位)或relative(相对定位)且z-index值不为auto的元素;position值为fixed(固定定位)或sticky(粘滞定位)的元素(沾滞定位适配所有移动设备上的浏览器,但老的桌面浏览器不支持);- flex (
flexbox) 容器的子元素,且z-index值不为auto; - grid (
grid) 容器的子元素,且z-index值不为auto; opacity属性值小于1的元素(参见 the specification for opacity);mix-blend-mode属性值不为normal的元素;- 以下任意属性值不为
none的元素: isolation属性值为isolate的元素;-webkit-overflow-scrolling属性值为touch的元素;will-change值设定了任一属性而该属性在 non-initial 值时会创建层叠上下文的元素(参考这篇文章);contain属性值为layout、paint或包含它们其中之一的合成值(比如contain: strict、contain: content)的元素。
第二点,需要剪裁(clip)的地方也会被创建为图层。
div {
width: 200;
height: 200;
overflow: auto;
background: gray;
}出现这种裁剪情况的时候,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层
下一代布局计算 LayoutNG
在执行布局操作的时候,会把布局运算的结果重新写回布局树中,所以布局树既是输入内容也是输出内容,这是布局阶段一个不合理的地方,因为在布局阶段并没有清晰地将输入内容和输出内容区分开来。针对这个问题,Chrome 团队正在重构布局代码,下一代布局系统叫 LayoutNG,试图更清晰地分离输入和输出,从而让新设计的布局算法更加简单。
图层绘制
渲染引擎实现图层的绘制,会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表,
栅格化(raster)操作
绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。
在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。
基于这个原因,合成线程会将图层划分为图块(tile),
然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。
通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。
GPU 操作是运行在 GPU 进程中,如果栅格化操作使用了 GPU,那么最终生成位图的操作是在 GPU 中完成的,这就涉及到了跨进程操作。渲染进程把生成图块的指令发送给 GPU,然后在 GPU 中执行生成图块的位图,并保存在 GPU 的内存中。
合成和显示
一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

综合上图,我们总结下渲染进程的流程:
- 渲染引擎将 HTML 内容转化成可以读懂的DOM 结构
- 将 CSS 文件转化成styleSheets,计算出 DOM 节点样式
- 合并生成layoutTree
- 按照不同的层叠上下文进行分层,创建不同图层
- 把一个图层的绘制拆分成很多小的绘制指令并将其提交到合成线程
- 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
- 生成绘制命令-“DrawQuad”,发送到浏览器进程
- viz 组件将内容绘制到内存,并显示到显示器上
重绘、重排和合成
重排
如果你通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。
重绘
如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。
合成
如果你更改一个既不要布局也不要绘制的属性,渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成。
使用 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。‘如果采用了 GPU,那么合成的效率会非常高。
需要重点关注的是,合成操作是在合成线程上完成的,这也就意味着在执行合成操作时,是不会影响到主线程执行的。
如何利用合成优化
使用 will-change,这段代码就是提前告诉渲染引擎 box 元素将要做几何变换和透明度变换操作,这时候渲染引擎会将该元素单独实现一帧,等这些变换发生时,渲染引擎会通过合成线程直接去处理变换,这些变换并没有涉及到主线程,这样就大大提升了渲染的效率。这也是 CSS 动画比 JavaScript 动画高效的原因。
.box {
will-change: transform, opacity;
}详细例子查看:will-change-code
Performance
总结
CSSOM 会阻塞渲染,只有当 CSSOM 构建完毕后才会进入下一个阶段构建渲染树。
通常情况下 DOM 和 CSSOM 是并行构建的,但是当浏览器遇到一个 script 标签时,DOM 构建将暂停,直至脚本完成执行。但由于 JavaScript 可以修改 CSSOM,所以需要等 CSSOM 构建完毕后再执行 JS。