渲染流水线
由于渲染机制过于复杂,所以渲染模块在执行过程中会被划分为很多子阶段,输入的 HTML 经过这些子阶段,最后输出像素。我们把这样的一个处理流程叫做渲染流水线,其大致流程如下图所示: 按照渲染的时间顺序,流水线可分为如下几个子阶段:构建 DOM 树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。
构建 DOM 树
为什么要构建 DOM 树呢?这是因为浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树。
样式计算(Recalculate Style)
样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式,这个阶段大体可分为三步来完成。
1. 把 CSS 转换为浏览器能够理解的结构
从图中可以看出,CSS 样式来源主要有三种:
- 通过 link 引用的外部 CSS 文件
- 元素的 style 属性内嵌的 CSS
和 HTML 文件一样,浏览器也是无法直接理解这些纯文本的 CSS 样式,所以当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets。
一个
2. 转换样式表中的属性值,使其标准化
现在我们已经把现有的 CSS 文本转化为浏览器可以理解的结构了,那么接下来就要对其进行属性值的标准化操作。 CSS 文本中有很多属性值,如 2em、blue、bold,这些类型数值不容易被渲染引擎理解,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。
3. 计算出 DOM 树中每个节点的具体样式
- 首先是 CSS 继承。**CSS 继承就是每个 DOM 节点都包含有父节点的样式。**这么说可能有点抽象,我们可以结合具体例子,看下面这样一张样式表是如何应用到 DOM 节点上的。
- **层叠是 CSS 的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。**它在 CSS 处于核心地位,CSS 的全称“层叠样式表”正是强调了这一点。
总之,样式计算阶段的目的是为了计算出 DOM 节点中每个元素的具体样式,在计算过程中需要遵守 CSS 的继承和层叠两个规则。这个阶段最终输出的内容是每个 DOM 节点的样式,并被保存在 ComputedStyle 的结构内。
布局阶段
1. 创建布局树
在显示之前,我们还要额外地构建一棵只包含可见元素布局树。 依赖dom树以及样式计算属性构建布局树 为了构建布局树,浏览器大体上完成了下面这些工作:
- 遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中;
- 而不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容,再比如 body.p.span 这个元素,因为它的属性包含 dispaly:none,所以这个元素也没有被包进布局树。
visibility: hidden的元素会出现在布局树中,这与display: none是不同的
2. 布局计算
分层
因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。 通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。 创建新图层要求:
- 拥有层叠上下文属性的元素会被提升为单独的一层。具有明确定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素等,都拥有层叠上下文属性。
- 需要剪裁(clip)的地方也会被创建为图层。出现这种裁剪情况的时候,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层。
剪裁:内容长度超出包裹元素的高度
图层绘制列表
渲染引擎实现图层的绘制与之类似,会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表
栅格化(raster)操作
绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。你可以结合下图来看下渲染主线程和合成线程之间的关系: 内容过多要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。 基于这个原因,合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512 然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,运行方式如下图所示: 通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。
合成和显示
一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
渲染流水线总结
重排/回流
重排是指更新了元素的几何属性 从上图可以看出,如果你通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。 "回流"(reflow)是指在网页渲染过程中,当 DOM 元素的尺寸、位置或某些属性发生变化时,浏览器需要重新计算并重新渲染受影响的部分的过程。回流通常是由于以下操作引起的:
- 修改了元素的尺寸、位置、内容等属性。
- 修改了页面的布局、样式等属性。
- 用户交互事件引起了元素尺寸、位置等的变化。
在前端开发中,因为回流会导致浏览器重新计算和渲染页面,所以会影响页面性能,特别是在元素频繁变化的情况下。为了减少回流次数,开发人员可以采用一些优化措施,如:
- 避免频繁操作 DOM,尽量在一个时间段内集中修改多个元素。
- 将频繁变化的元素设置为 position: absolute 或 fixed,这样它们不会影响其他元素的位置。
- 避免通过 JavaScript 直接操作样式,尽量使用 class 名称来控制样式的变化。
- 将元素的尺寸、位置等属性缓存起来,避免多次访问和计算。
重绘
重绘是指更新元素的绘制属性
合成
直接合成阶段 那如果你更改一个既不要布局也不要绘制的属性,会发生什么变化呢?渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成。具体流程参考下图: