浏览器渲染流程

渲染流水线

browser_render_pipeline

浏览器的渲染机制很复杂,渲染模块在执行的过程中会被划分为很多个子阶段,输入html经过这些子阶段,最后输出像素。我们把这个处理流程叫做“渲染流水线”。

大致表达如上图。

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

  • 构建DOM树
  • 样式计算
  • 布局阶段
  • 分层
  • 绘制
  • 分块
  • 光栅化合成。

接下来在介绍每个阶段的过程中,你应该关注以下三个重点:

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

构建DOM树

为什么要构建DOM树?因为浏览器无法直接理解和使用HTML,所以需要把HTML转换为浏览器能够理解的结构——DOM树。

build_dom_tree

另外打开Chrome开发者工具,选择console,控制台中输入“document” 回车,这样看到额就是完整的DOM树。

样式计算(Recalcuate Style)

为了计算出DOM节点每个元素的具体样式,这个阶段大概可分为散步

1. 把CSS转换为浏览器能够理解的结构

CSS的来源主要有三种

  • 通过link引用的外部CSS文件
  • 元素style属性内嵌的CSS

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

Chrome控制台,输入 document.styleSheets 就可以看到。

这个样式表已经把那三种来源的样式都包含进去了。该结构同时具备了查询和修改的功能,这会为后面的央视操作提供基础。

2.转换像是表中的属性值,使其标准化

比如如下代码,其中有很多类型数值,不被浏览器理解,所以需要把所有值转换为渲染引擎理解的标准化的计算值。

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

标准化后为

body { font-size: 32px }
p {color: rgb(0,0,255);}
span {display: none}
div {font-weight: 700}
div p {color: rgb(0,128,0);}
div {color: rgb(255,0,0); }

可以看到一些值已经被替换。

3.计算出DOM树中每个节点的具体样式

样式属性已经被标准化,接下来需要计算DOM中每个节点的样式属性。

这就涉及到CSS的继承规则和层叠规则。

  1. 首先是CSS继承,CSS继承就是每个DOM节点都包含有父节点的样式。计算过程中,会根据DOM节点的继承关系来合理计算节点样式。

  2. 样式计算的第二个规则是样式层叠。 层叠是CSS的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。它在CSS处在核心的地位。CSS全称是“层叠样式表 正是强调了这一点。

布局阶段

我们有了DOM树、DOM树中元素的样式,但是依然无法显示,因为我们还不知道DOM的几何位置信息。

接下来就要计算DOM树中可见元素的几何位置——这个计算过程叫做“布局”。

Chrome在布局阶段需要完成两个任务

  • 创建布局树
  • 布局计算

1.创建布局树

DOM树还包含很多不可见元素,比如 head标签,display:none属性元素。所以在显示之前,我们需要额外的创建一颗只包含可见元素的布局树。

browser_build_layout_tree

为了构建布局树,浏览器大概完成下面工作:

  1. 遍历DOM树中所有可见节点,并把这些节点加到布局树中

  2. 不可见节点会被忽略。比如head标签,比如 display:none的元素

2.布局计算

现在我们有了一颗完整的布局树,就要计算布局树节点的表坐标位置。布局计算的过程非常复杂。这里先跳过。后面再做详细介绍。

在执行布局操作的时候,会把布局计算的结果重新写回布局树,所以布局树既是输入内容,也是输出内容。这是布局阶段一个不合理的地方。针对这个,Chrome团队正在重构布局代码,下一代布局系统叫做LayoutNG。

中间总结

browser_render_pipeline2

浏览器不能直接理解HTML数据,所以第⼀步需要将其转换为浏览器能够理解的DOM树结 构;⽣成DOM树后,还需要根据CSS样式表,来计算出DOM树所有节点的样式; 最后计算DOM元素的布局信息,使其都保存在布局树中。

分层

拥有了布局树,每个元素的具体位置都计算出来了,接下来可以绘制页面么?

还不行。

页面中有很多复杂的效果,比如一些3D变换,页面滚动或者z-index的z轴排序。为了方面实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并且生成一颗对应的图层树(LayerTree)。 使用过 PhotoShop,绘画软件应该很容易理解图层的概念。这些图层叠加在一起构成了最终的页面图像。

browser_layer_tree

通常,并不是每个布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。最终每个节点都会直接或者间接的从属于一个层。

什么情况下会新建图层呢?

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

页面是个二维平面,层叠上下文可以让HTML元素具有三维的概念,这些HTML按照自身属性的优先级分布在垂直于这个二维平面的z轴上

browser_css

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

层叠上下文参考

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

结合一下代码

<style>

div {
    width:200;
    height:200;
    overflow:auto;
    background: gray;
}

</style>

<body>
    <div>
        <p>所以元素有了层叠上下⽂的属性或者需要被剪裁,那么就会被提升成为单独⼀层,你可以参看下图:</p>
        <p>从上图我们可以看到,document层上有A和B层,⽽B层之上⼜有两个图层。这些图层组织在⼀起也是⼀颗树状结
        <p>图层树是基于布局树来创建的,为了找出哪些元素需要在哪些层中,渲染引擎会遍历布局树来创建层树
    </div>
</body>

在这⾥我们把div的⼤⼩限定为200 * 200像素,⽽div⾥⾯的⽂字内容⽐较多,⽂字所显示的 区域肯定会超出200 * 200的⾯积,这时候就产⽣了剪裁,渲染引擎会把裁剪⽂字内容的⼀部 分⽤于显示在div区域。出现这种裁剪情况的时候,渲染引擎会为⽂字部分单独创建⼀个层,如果出现滚动条,滚动 条也会被提升为单独的层。

所以说,元素有了层叠上下⽂的属性或者需要被剪裁,满⾜其中任意⼀点,就会被提升成为 单独⼀层。

图层绘制

在完成图层构建之后,渲染引擎会对图层树中每个图层进行绘制,接下来看渲染器引擎怎么实现图层绘制的?

试想一下,如果给你一张纸,让你先把纸背景涂成蓝色,然后在中间位置画一个红色圆,然后再在圆上画个绿色三角形,你会怎么操作?

通常你会把绘制过程分解为三部:

  1. 绘制蓝色背景
  2. 在中间绘制一个红色的圆
  3. 再在圆上绘制绿色三角形

渲染引擎在实现图层绘制的与这个类似,会把一个图层的绘制拆分成很多小的“绘制命令”,然后再把这些命令按照顺序组成一个待绘制列表。

browser_draw_queue

也可以在开发者工具“Layers”(设置里可以勾选展示),选择“document”层,来实际体现下绘制列表。

栅格化(Raster) 操作

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

browser_composite_thread

如图,当图层绘制列表准备好后,主线程会把该绘制列表提交(Commit)给合成线程,那么接下合成线程怎么工作呢?

先来看看新的概念,视口

视口(ViewPort)

把完整的网页想象成一张长长的图片,而我们看到的只是屏幕这个矩形框里面的一部分。屏幕上页面的可见区域,就叫做 视口(ViewPort).

有的图层可以很大,比如有的页面你需要滚动好久才能到底,但是通过视口,用户只能看到页面的很小一部分,所以这种情况下,要绘制出所有涂层的内容就会产生太大的开销,而且完全没有必要。

基于这个原因,合成线程会将图层划分为图块(tile),这些图快的大小通常是256x256或者512x512。

然后合成线程会按照视口附近的土块来优先合成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,就是将图块转化为位图。

而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的。

browser_raster

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

GPU操作是运行在GPU进程中,如果栅格化操作使用了GPU那么最终生成位图的操作是在GPU中完成的,这就涉及到跨进程操作,可以参考下图。

browser_gpu_raster

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

合成(Composite)和显示

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

浏览器进程里有一个叫做 viz的组件,用来接收合成线程发过来的 DrawQuad命令,然后根据DrawQuad命令,将其页面内容会知道内存中,最后再将内存显示在屏幕中。

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

browser_full_render_progress

总结

  1. 渲染进程将HTML内容转换为能够读懂的DOM树结构。

  2. 渲染引擎将CSS样式表转化为浏览器可以理解的styleSheets,计算出DOM节点的样式。

  3. 创建布局树,并计算元素的布局信息。

  4. 对布局树进⾏分层,并⽣成分层树。

  5. 为每个图层⽣成绘制列表,并将其提交到合成线程。

  6. 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。

  7. 合成线程发送绘制图块命令DrawQuad给浏览器进程。

  8. 浏览器进程根据DrawQuad消息⽣成⻚⾯,并显示到显示器上。

相关概念

介绍完渲染流水线基础,再来看三个和渲染流水线相关的概念—— 重排、重绘、合成

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

browser_render_reflow

如果你通过JavaScript或者CSS修改元素的几何位置属性,例如改变元素的宽高,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程叫做 重排Reflow。

无疑,重排需要更新完整的渲染流水线,所以开销是最大的。

2. 更新元素的绘制(重回 Repaint)

比如通过JS更改某些元素的背景颜色渲染流水线

browser_render_repaint

图中可以看到,如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为没有引起几何位置变换,所以直接进入绘制阶段,然后执行之后的一些列子阶段。

这个过程叫做 重绘。

相较于重排,重绘操作省去了布局和分层阶段,所以执行效率比重排操作高。

3. 直接合成(Composite)阶段

browser_render_composite

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

Mark24

Everything can Mix.