浏览器渲染流程

110 阅读6分钟

浏览器渲染流程

由于渲染流程过于复杂,所以渲染模块在执行中会被划分为几个子阶段,输入的 htmlcssjs 经过这些子阶段,最后输出像素,我们把这样的一个处理流程叫做渲染流水线

按照时间顺序,流水线可分为如下几个子阶段:

  1. 构建 DOM 树
  2. 样式计算
  3. 布局阶段
  4. 分层
  5. 绘制
  6. 分块
  7. 光栅化
  8. 合成

在每个阶段的过程中,都要关注以下三点内容:

  • 开始时,每个子阶段都有其输入的内容
  • 每个子阶段都有其处理过程
  • 每个子阶段都会生成输出内容

构建 DOM 树

因为浏览器无法直接理解和使用 HTML,所以需要将其转化为浏览器能够理解的结构 —— DOM 树

DOM树的构建.png

从图中可以看出,HTML 文件经由 HTML 解析器 解析,最终输出树状结构的 DOM。

为了直观的理解,你可以写一个简单的文件,控制台打印一下 document

DOM树可视化.png

从上图可以看到,DOM树 和 HTML 的内容几乎是一样的,但是和 HTML 不同的是,DOM树 是保存在内存中的,可以通过 js 来查询和修改其内容。

现在 DOM 树已经生成了,但是 DOM 节点的样式还不知道,要让 DOM 节点拥有正确的样式,还需要样式计算。

样式计算

计算出每个元素的具体样式,这个阶段大体可以分为三步完成。

  1. 把 css 转换为浏览器能够理解的结构
  2. 转换样式表中的属性值,使其标准化
  3. 计算出具体的节点样式

把 css 转换为浏览器能够理解的结构

当渲染引擎接收到 css 文本时(link引入的、style里面的,行内样式),会将其转换为浏览器可以理解的结构——styleSheets

控制台打印 document.styleSheets:可以看到如下结构,并且该结构同时具备了查询和修改功能

styleSheets.png

转换样式表中的属性值,使其标准化

body { font-size: 2em }
p {color:blue;}
span  {display: none}
div {font-weight: bold}
div  p {color:green;}
div {color:red; }

css 文本中有很多属性值,如 2em、blue、bold,这些类型数组不容易被引擎理解,所以要将其转化为标准化的值。 标准化属性值.png

计算出具体的节点样式

属性值也已经被标准化了,那么如何计算每个节点的样式属性呢。

要根据 css 的继承规则层叠规则 来计算最终的节点样式。并被保存在 ComputedStyle 的结构内

元素最终计算的样式.png

布局阶段

布局阶段就需要计算出 DOM 树中可见元素的几何位置。

此阶段需要完成两个任务:

  1. 创建布局树
  2. 布局计算

创建布局树

DOM 树中还含有很多不可见元素,比如 head,还有使用了 display:none 属性的元素。所有我们还要额外构建一颗只包含可见元素的布局树。

布局计算

有了完整的布局树之后,就要计算布局树的坐标位置了。

分层

现在我们有了布局树和具体的位置信息,是不是就要着手绘制页面了?答案是否定的。

因为页面中有很多复杂的效果,如 3D 变化,页面滚动,或者使用 z-index 属性,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一颗对应的图层树。

多图层示意图.png

浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面

绘制

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

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

图层绘制列表.png

分块

当绘制列表准备好之后,渲染的主进程会把该列表提交合成线程

有的图层很大,但是通过视口,用户只能看到那一部分,所以在这种情况下,绘制出所有图层内容的话,产生的开销比较大,而且没有必要,基于这个原因,合成线程会将图层划分为图块

合成线程会按照视口附近的图块优先生成位图。

生成位图的操作是由栅格化执行的

光栅化

所谓栅格化,是指将图块转为位图。而图块是栅格化执行的最小单位。

渲染进程维护一个栅格化的线程池,栅格化的操作就是再此执行的。

通常,栅格化的过程都会使用 GPU 加速生成,所以也叫 快栅格化、GPU 栅格化,生成的位图保存在 GPU 内存中。

合成

所有图块都被光栅化之后,合成线程就会生成一个绘制图块的命令 DrawQuad 给浏览器进程。

浏览器进程中存在一个 viz 组件,用来接收该命令,然后将其内容绘制到内存中,最后再将内存显示到屏幕上。

总结

  1. 渲染进程将 HTML 内容转换为 DOM 树结构
  2. 将 css 样式转化为 styleSheets,计算 DOM 节点的样式
  3. 创建布局树,并计算元素的布局信息
  4. 对布局树进行分层,生成分层树
  5. 为每个图片生成绘制列表,将其提交给合成线程
  6. 合成线程将图片分为图块,并在光栅化线程池中将图块转换为位图
  7. 合成线程发送命令给浏览器进程
  8. 浏览器进程接收消息,合成页面,并将其显示。

相关概念

重排、重绘、合成

  • 更新元素的几何元素(重排)
    • 如果你通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的
  • 更新元素的绘制属性(重绘)
    • 如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些
  • 直接合成
    • 那如果你更改一个既不要布局也不要绘制的属性,会发生什么变化呢?渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成
    • 我们使用了 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率