Chrome 浏览器的渲染原理

594 阅读6分钟

一、构建 DOM 树

image

构建 DOM 树的输入内容是一个非常简单的 HTML 文件,然后经由 HTML 解析器解析,最终输出树状结构的 DOM。

二、样式计算 Recalculate Style

样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式。

和 HTML 文件一样,浏览器也是无法直接理解这些纯文本的 CSS 样式,所以当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets

image

通过 styleSheets,计算出 DOM 树中每个节点的具体样式:即 ComputedStyle

image

三、布局阶段 Layout & Layer

现在,我们有 DOM 树和 DOM 树中元素的样式,但这还不足以显示页面,因为我们还不知道 DOM 元素的几何位置信息。那么接下来就需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局

Chrome 在布局阶段需要完成两个任务:创建布局树和创建布局图层。

  • 1. 创建布局树 Layout

    你可能注意到了 DOM 树还含有很多不可见的元素,比如 head 标签,还有使用了 display:none 属性的元素。所以在显示之前,我们还要额外地构建一棵只包含可见元素布局树

    我们结合下图来看看布局树的构造过程:

    image

  • 2. 布局图层 Layer

    现在我们有了布局树,而且每个元素的具体位置信息都计算出来了,那么接下来是不是就要开始着手绘制页面了? NO!

    • 分层

      因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。

      image

    ​ 布局树和图层树之间的关系:

    image

四、图层绘制

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

当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)合成线程

  • 合成线程

    合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512,如下图所示:

    image

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

    image

    一般情况下,会使用 GPU栅格化 来加速位图生成。

    其实,栅格化线程池就是一个“加工厂”,将图块转化为位图。

  • 合成和显示

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

    到这里,经过这一系列的阶段,编写好的 HTML、CSS、JavaScript 等文件,经过浏览器就会显示出漂亮的页面了。

渲染流水线大总结

从 HTML 到 DOM、样式计算、布局、图层、绘制、光栅化、合成和显示。下面我用一张图来总结下这整个渲染流程:

image

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

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

相关概念

我们再来看看三个和渲染流水线相关的概念——“重排”“重绘”和“合成”

这对于 Web 的性能优化会有很大帮助。

1. 重排 reflow

更新了元素的几何属性。

image

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

2. 重绘 repaint

更新元素的绘制属性。

image

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

3. 直接合成阶段

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

image

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

减少 reflow/repaint 方案列举

  1. 不要一条一条地修改DOM的样式。与其这样,还不如预先定义好css的class,然后修改DOM的className。
// bad
var left = 10,
top = 10;
el.style.left = left + "px";
el.style.top  = top  + "px";
 
// Good
el.className += " theclassname";
 
// Good
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
  1. 把DOM离线后修改。如:
  • 使用 DocumentFragment 对象在内存里操作DOM
  • 先将 DOM display: none (有一次reflow),然后你想怎么改就怎么改。比如修改100次,然后再把他显示出来。
  • clone 一个 DOM节点 到内存里,然后想怎么改就怎么改,改完后,和在线的那个的交换一下。
  1. 不要把 DOM节点 的属性值放在一个循环里当成循环里的变量。

    不然这会导致大量地读写这个节点的属性。

  2. 尽可能的修改层级比较低的DOM。

    当然,改变层级比较底的DOM有可能会造成大面积的reflow,但是也可能影响范围很小。

  3. 为动画的HTML元件使用position: fixed 或 absoulte 。

    那么修改他们的 css 是不会 reflow 的。

  4. 千万不要使用table布局。

    因为可能很小的一个小改动会造成整个table的重新布局。