0%

浏览器原理学习笔记-页面渲染流程(下)

分层

因为页面中还有一些复杂的动画效果,例如一些复杂的 3D 变化、页面滚动。为了更方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一颗对应的图层树(LayerTree)。

想要更直观地理解图层,可以打开 Chrome 的“开发者工具”, 选择“Layers”标签,就可以看到可视化页面的分层情况。

渲染引擎给页面分了很多图层,这些图层按照一定的顺序叠加在一起,就形成了最终的页面。

图层和布局节点树之间的关系为:

image-20200529152538242

并不是布局树的每个节点都包含一个图层,如果一个节点没有相对应的层,那么这个节点就属于父节点的图层。

上图中 span 标签没有专属图层,那么它就从属于它的父节点图层。不管怎么样,最终每个节点都会直接或间接从属于一个层。

一般渲染引擎会为满足下面条件的节点创建新的图层:

1.拥有层叠上下文属性的元素会被提升为单独的一层

页面是个二维页面,但是层叠上下文能够让 HTML 元素具有三维概念,这些 HTML 元素按照自身属性的优先级分布在垂直于这个二维平面的 z 轴上,根据下图来感受:

image-20200529153313693

从图中可以看出,明确定位元素的属性、定义透明属性的元素、使用 CSS 滤镜的元素等都有层叠上下文属性。

2.需要裁剪(clip)的地方也会被创建为图层

裁剪例如,设置了overflow: auto 属性的 div 元素。超出了限定的面积之后那么div里面的内容就会被裁剪。

这种情况下,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会成单独一层。

图层绘制

完成图层构建之后,渲染引擎就会对图层树中的每个图层进行绘制。

渲染引擎会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表,如下图:

image-20200529160103665

从图中可以看出,绘制列表中的指令其实很简单,就是让其执行一个简单的绘制操作,比如绘制线条之类的。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。所以在图层绘制阶段,输出的列表就是这些待绘制列表。

栅格化(raster)操作

绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎的合成线程来完成的。可以结合下图来查看渲染主线程和合成线程之间的关系:

image-20200529161218832

如上图,当图层的绘制列表准备好之后,主线程会把绘制列表提交(commit)结合成线程,那么接下来合成线程是怎么准备工作的呢?

首先我们先知道一个叫做视口的概念:一个页面可能很大,但是用户只能看到其中的一部分,那么这一部分就是视口。

合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转成位图。图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,运行方式如下图:

image-20200529163059152

通常,这个过程会被 GPU 来加速生成,使用 GPU 生成位图的过程叫做快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。

GPU 操作是运行在 GPU 进程中的,栅格化操作使用了 GPU,最终生成位图的操作是在 GPU 中完成的。这就涉及到了跨进程操作:

image-20200529163433987

从图中可以看出,渲染进程把生成图块的指令发送给 GPU,然后在 GPU 中执行生成图块的位图,并保存在 GPU 的内存中。

合成和显示

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。

浏览器进程里面有个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,根据此命令,将其内容绘制到内存中,然后再将内存显示在屏幕上。

到这里,html、css、js 等文件经过浏览器就会显示出漂亮的页面了。

渲染流水线总结

总结图为:

image-20200529165109936

结合上图,一个完整的渲染流程大致可总结为:

  1. 渲染进程将 HTML -> DOM
  2. 渲染引擎将 CSS -> StyleSheets,计算出 DOM 样式的节点
  3. 创建布局树,并计算元素的布局信息
  4. 对布局树分层,并生成分层树
  5. 为每个图层生成绘制列表,并将其提交到合成线程
  6. 合成线程将图层分块,并在光栅化线程池中将图块转换成位图
  7. 合成线程发送图块绘制命令 DrawQuad 给浏览器进程
  8. 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上面

相关概念

三个关于渲染流水线的相关概念——“重排”、“重绘”、“合成”。

1.更新了元素的几何属性(重排)

image-20200529171002628

RT,通过 js 或 css 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器就会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的

2. 更新元素的绘制属性(重绘)

如图所示:

image-20200529171409707

如果 js 修改了元素的背景颜色,那么布局阶段将不会被执行,因为没有引起几何位置的变化,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重绘,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些

3.直接合成阶段

如果改一个既不要布局也不要绘制的属性,会发生什么变化呢?渲染引擎将跳过布局和绘制,只执行后续的合成操作,那么这个过程就叫做合成。具体流程为:

image-20200529171759530

上图中使用了 CSS 的 transform 来实现动画,这可以避开重排和重绘阶段,直接在非主线程执行合成动画的操作。这样的效率是最高的。因为是在非主线程上合成,并且没有占用主线程和资源,另外也避开了布局和绘制两个阶段。因此,合成能够大大提升绘制效率

QA

减少重排重绘, 方法很多:

  1. 使用 class 操作样式,而不是频繁操作 style
  2. 避免使用 table 布局
  3. 批量dom 操作,例如 createDocumentFragment,或者使用框架,例如 React
  4. Debounce window resize 事件
  5. 对 dom 属性的读写要分离
  6. will-change: transform 做优化