渲染流程:HTML、CSS和JavaScript,是如何变成页面的?

64 阅读9分钟

前言

上一篇文章《从输入URL到页面展示,这中间发生了什么?》,我们已经大致了解了导航相关的流程,那导航之后,就进入了渲染流程。这个阶段很重要!了解其流程能让我们“看透”页面的工作流,从而去优化一些页面卡顿问题,优化动画流程等。

渲染流程

接着上文,渲染进程接收完文档之后,浏览器进行页面的更新。下一步就是渲染进程解析资源,并显示。

输入 HTML、CSS、JS,经过一系列子阶段的处理,最终输出像素,这个处理流程叫做渲染流水线。大致示意图如下:

截屏2023-09-02 21.34.13.png

按照渲染的时间顺序,子阶段有:构建 DOM 树、样式计算、布局、分层、绘制、分块、光栅化和合成。

每个子阶段都有对应的 输入内容,处理方法、输出内容,理解了这三部分,就能更好的理解每一个子阶段。

构建 DOM 树(DOM)

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

截屏2023-09-02 22.25.44.png

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

DOM 是保存在内存中树状结构,可以通过 JavaScript 来查询或修改其内容。

样式计算(StyleComputed)

样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式,分三步:

1、CSS 转化styleSheets

把 CSS 转换为浏览器能够理解的结构,CSS的来源有:

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

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

控制台中,输入 document.styleSheets,就能查看其结构:

截屏2023-09-02 22.38.16.png

2、属性值标准化

得到样式表后,转换其中的属性值,使其标准化。 比如以下样式:

p { font-size: 2em }
div { color: bule }

CSS 文本中有很多属性值,如 2em、blue,这些类型数值不容易被渲染引擎理解,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化

截屏2023-09-03 11.14.10.png

上图结果:2em 被解析成了 32px, blue 被解析成 rgb(0,0,255)。

3、计算出节点的具体样式

属性被标准化后,接着计算每个节点的具体样式,里面涉及到 CSS 的继承规则和层叠规则

CSS 继承

每个 DOM 节点都包含有父节点的样式。比如

image.png

打开开发者工具,看到如下界面,显示了 css 的属性继承过程:

image.png

样式层叠

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

image.png

上图红色方框中显示了 html.body.div.p 标签的 ComputedStyle 的值,即最终的计算样式。

布局 (Layout)

有 DOM 树和 DOM 树中元素的样式,接下来就需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局

创建布局树:根据 DOM 树 和 计算的样式,构建一颗只包含可见元素的布局树。

布局计算:计算布局树节点的坐标位置。

image.png

从上图可以看出,DOM 树中所有不可见的节点都没有包含到布局树中。

为了构建布局树,浏览器大体上完成了下面这些工作

  • 遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中
  • 而不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容及 display:none 的元素。

分层(Layer)

浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。

通常满足下面两点中任意一点的元素就可以被提升为单独的一个图层

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

如:明确位置的属性(position)、定义页面层级的属性(z-index)、css 滤镜属性(filter)、定义透明度的属性(opacity)等

image.png

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

当元素内容的显示区域超过元素定义的大小时,就会产生裁剪。出现这种裁剪情况的时候,渲染引擎会为内容部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层.

结合以下列子来理解:

<style>
div {
  width: 200px;
  height: 200px;
  overflow:auto;
  background: gray;
}
</style>
<dody>
    <div>
      <p>当元素内容的显示区域超过元素定义的大小时,就会产生裁剪。出现这种裁剪情况的时候,渲染引擎会为内容部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层.</p>
      <p>当元素内容的显示区域超过元素定义的大小时,就会产生裁剪。出现这种裁剪情况的时候,渲染引擎会为内容部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层.</p>
    </div>
</body>

打开控制台查看图层:

image.png

有三个图层:document、div、滚动条

图层绘制 (Paint)

接下来对每个图层进行绘制。

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

所以图层绘制阶段,就是输入图层,输出图层待绘制列表。

光栅化(Raster)

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

image.png

通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)。

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

所以,合成线程会将图层划分为图块(tile)。

合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。

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

image.png

合成与显示

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

浏览器进程根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

image.png

本文总结

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

  • DOM: 渲染进程将 HTML 内容转换为 DOM 树结构;
  • Style: 渲染引擎将 CSS 样式表转化为 styleSheets,计算出 DOM 节点的样式。
  • Layout: 创建布局树,并计算元素的布局信息
  • Layer: 对布局树进行分层,并生成分层树
  • Paint: 为每个图层生成绘制列表,并将其提交到合成线程。
  • Raster: 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
  • DrawQuad: 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
  • Display: 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。

从输入URL到页面展示-接上文总结

1、用户输入

  • 1.1、协议组装;
  • 1.2、IPC: 把url请求发送给网络进程;
  • 1.3、查找本地缓存;
  • 1.4、DNS 解析:获取服务器ip地址;
  • 1.5、建立 TCP 连接;
  • 1.6、构建请求头信息;
  • 1.7 发送请求头信息;

2、响应阶段

  • 2.1、状态码解析:301 / 302 / 200;
  • 2.2、响应类型的处理:Content-type;
  • 2.3、Content-type=text/html,准备渲染进程进行解析;

3、提交阶段

  • 3.1、分配渲染进程:同站点复用父渲染进程;
  • 3.2、浏览器进程向渲染进程发起“提交文档”的消息;
  • 3.3、渲染进程接收后,与网络进程建立消息传输通道;
  • 3.4、文档传输完后,向浏览器进程发送“确认提交”的消息;
  • 3.5、浏览器进程接收后,更新浏览器界面等信息;

4、渲染阶段

  • 4.1、构建DOM树;
  • 4.2、样式计算;
  • 4.3、布局定位;重排;
  • 4.4、图层分层:生成图层树;
  • 4.5、图层绘制:重绘。生成绘制指令,输出待绘制列表;
  • 4.6、光栅化:将图块转换成位图;
  • 4.7、浏览器进程接收后,更新浏览器界面等信息;

相关概念

重排

通过 JavaScript 或者 CSS 修改元素的几何位置属性,浏览器会触发重新布局,这个过程叫 重排

image.png

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

重绘

更新元素的绘制属性,比如通过 JavaScript 更改某些元素的字体颜色,浏览器会触发重新绘制,这个过程叫 重绘

image.png

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

如何减少重排重绘

减少重排重绘,就等于减少渲染进程的主线程和非主线程的操作和计算,能够加快页面的展示。

  • 1、使用class 样式,避免频繁的操作 style。
  • 2、批量dom操作,一起提交,如使用 createDocumentFragment创建虚拟节点。
  • 3、will-change: transform 做优化
  • ....

createDocumentFragment

创建文档片段,文档片段不在 DOM 中,因此对其操作不会引起页面回流,起到优化作用。

如:在ul标签种动态添加多个li列表

var ul = document.getElementById("ul");
var fragment = document.createDocumentFragment();
for (var i = 0; i < 20; i++) {
    var li = document.createElement("li");
    li.innerHTML = "index: " + i;
    fragment.appendChild(li);
}
ul.appendChild(fragment)