Skip to main content

浏览器渲染过程

浏览器是一个边解析边渲染的过程。

浏览器解析渲染页面分为一下五个步骤:

  1. 构建 DOM 树
  2. 构建 CSS 规则树
  3. 二者结合生成 Render 树
  4. 再生成布局
  5. 然后绘制到页面中

一、构建 DOM 树

由于浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构 DOM 树。在此步骤中,HTML 解析器会将输入的 HTML 文档解析为对应的 DOM 树。

DOM 树是如何构建的?

  1. 转码: 浏览器对收到的二进制数据按指定编码格式转为 HTML 字符串;
  2. 生成 Tokens: 然后将 HTML 字符串解析成 Tokens;
  3. 构建节点: 进行深度优先遍历,先构建当前节点的所有子节点,再构建下一个兄弟节点,以此类推;
  4. 生成 DOM 树: 最后通过节点的指针关系构建出 DOM 树。

二、构建 CSS 规则树

DOM 树只是页面的结构,要知道页面长什么样子,还需要知道 DOM 每一个节点的样式。

1、转换 CSS 文本

将 CSS 文本转换为浏览器可理解的结构 styleSheets(即 CSS 规则树 -> CSSOM 树)

由于匹配节点对应 CSS 规则时,是按照从右到左的顺序的,例如:div p { font-size :14px } 会先寻找所有的 p 标签然后判断它的父元素是否为 div,所以我们写 CSS 时,要尽量减少层叠嵌套。

2、标准化样式属性值

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

如 2em、blue、bold,这些类型数值不容易被渲染引擎理解,则会转换。将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。

解析 CSS 规则树时 JS 执行将暂停,直至 CSS 规则树就绪。浏览器在 CSS 规则树生成之前不会进行渲染。

三、二者结合生成渲染树

DOM 树和 CSS 规则树全部准备好了以后,浏览器才会开始生成渲染树。

1、渲染树

渲染树只包括需要显示的节点和这些节点的样式信息,不需要渲染的节点会被忽略,比如设置了 display:none 的节点,就不会在渲染树中显示。注意 visibility:hidden 属性并不算不可见属性,它的语义是隐藏元素,但元素仍然占据着布局空间,所以它会被渲染成一个空框。

精简 CSS 并可以加快 CSS 规则树的构建,从而加快页面相应速度。

2、渲染阻塞

浏览器在读取 HTML 文档、构建 DOM 树的过程中,如果遇到 <script> 标签,渲染引擎会将 DOM 树的构建暂停,先去加载执行 JS 代码,直至脚本执行完毕再继续构建 DOM 树,这就是所谓的渲染阻塞。这样做的原因在于 JS 代码可能会改变 DOM 的结构(比如执行 document.write() 等 API)

JS 不单会阻塞 DOM 构建,它会导致 CSSOM 去阻塞 DOM 树的构建。一旦引入了 JS,只有 CSSOM 构建完后,才会继续脚本执行和 DOM 树的构建。

换句话说:

  • JS 会阻塞后面的 DOM 解析
  • CSS 会阻塞 JS 执行

因此 <script> 的位置很重要,在实际使用过程中遵循以下两个原则:

  • CSS 优先:引入顺序上,CSS 资源先于 JS 资源。
  • JS 置后:通常把 JS 代码放到页面底部(</body> 前),且 JS 应尽量少影响 DOM 的构建。

如果要改变阻塞模式,可以在 <script> 标签上添加 async 或 defer 属性,浏览器会异步的加载和执行 JS 代码,而不会阻塞渲染。

四、生成布局

当浏览器生成渲染树以后,就会根据渲染树来进行布局(回流),这一阶段浏览器要做的事就是要弄清楚各个节点在页面中的确切位置和大小。具体过程如下:

1、创建布局树

创建建一棵只包含可见元素布局树。浏览器会做如下操作: 遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中;而不可见的节点会被布局树忽略掉。

2、布局计算

计算布局树节点的坐标位置。在执行布局操作的时候,会把布局运算的结果重新写回布局树中,所以布局树既是输入内容也是输出内容,这是布局阶段一个不合理的地方,因为在布局阶段并没有清晰地将输入内容和输出内容区分开来。针对这个问题,Chrome 团队正在重构布局代码,下一代布局系统叫 LayoutNG,试图更清晰地分离输入和输出,从而让新设计的布局算法更加简单。

3、分层

为了更方便的实现页面中的复杂的效果,如 3D 变换、页面滚动等,渲染引擎会为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree),即将多个图层叠加在一起构成最终的页面图像(打开 Chrome 的“开发者工具”,选择“Layers”标签即可看到页面的分层)

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

关于图层有如下几点需要注意:

  • 布局树并非每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层;
  • 拥有层叠上下文属性(定位属性、透明属性、CSS 滤镜、z-index 等)的元素会被提升为单独的一层;
  • 需要剪裁(clip)的地方也会被创建为图层。

布局完成后,如果某个部分发生了变化影响了布局,那就需要倒回去重新渲染,即回流

五、绘制页面

绘制阶段,系统会遍历渲染树,并调用渲染器的 paint() 方法将其内容显示在屏幕上。

1、图层绘制

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

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

绘制列表中的指令其实非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等。

而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。

所以在图层绘制阶段,输出的内容就是这些待绘制列表(打开开发者工具的 Layers 标签,选择 document 层,显示的则是绘制列表)

2、栅格化(raster)操作

绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程。

渲染主线程和合成线程之间的关系如下:

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

  • 光栅化(rasterizing):绘制页面,要把结构、样式等信息转化为显示器中像素的转化过程。
  • 视口:用户在页面中看到的部分。
  • 划分图块的原因:通过视口,用户只能看到页面的一部分,这种情况下如果绘制出所有图层内容会产生太大的开销,而且也没必要。
  • 栅格化:将图块转换为位图。

合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。

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

合成线程提交图块给栅格化线程池如下:

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

注意:GPU 操作是运行在 GPU 进程中,如果栅格化操作使用了 GPU,那么最终生成位图的操作是在 GPU 中完成的,这就涉及到了跨进程操作。即渲染进程把生成图块的指令发送给 GPU,然后在 GPU 中执行生成图块的位图,并保存在 GPU 的内存中。

3、合成和显示

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——DrawQuad,然后将该命令提交给浏览器进程。浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

如果某个元素的背景颜色,文字颜色等发生改变,不影响元素周围或内部布局的属性,将只会引起浏览器的重绘