渲染流水线

122 阅读10分钟

渲染流水线

根据李兵老师的《浏览器工作原理》中05、06、22、23、24章节对于渲染流水线模块做一个总结

渲染流水线的子阶段

  1. 构建 DOM 树
  2. 构建 CSSOM 树(样式计算)
  3. 布局
  4. 分层
  5. 绘制
  6. 光栅化
  7. 合成

每一个子阶段都有自己的输入内容处理过程输出内容

构建DOM树

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

  • 从页面的视角来看,DOM 是生成页面的基础数据结构。
  • 从 JavaScript 脚本视角来看,DOM 提供给 JavaScript 脚本操作的接口,通过这套接口,JavaScript 可以对 DOM 结构进行访问,从而改变文档的结构、样式和内容。
  • 从安全视角来看,DOM 是一道安全防护线,一些不安全的内容在 DOM 解析阶段就被拒之门外了。

输入内容:HTML 文件。

处理过程:HTML 解析器。 image.png

HTML 解析器
HTML 解析器开始工作流程
  1. 网络进程接收到数据后,根据content-type值为“text/html”判断为一个 HTML 文件。随后为该请求选择(若浏览器进程中同站点已存在渲染进程则会复用渲染进程)或者创建一个渲染进程。
  2. 网络进程和渲染进程之间会建立一个共享数据的管道,网络进程接收到数据后就往这个管道里面放,而渲染进程则从管道的另外一端不断地读取数据,并同时将读取的数据“喂”给 HTML 解析器(网络进程加载多少数据,HTML 解析器便解析多少数据)。HTML 动态接收字节流,并将其解析为 DOM。
DOM生成流程

20250622-203459.png

  1. 通过分词器将字节流转换为 Token 通过分词器先将字节流转换为一个个 Token,分为 Tag Token 和文本 Token。

20250622-203722.png 2. 将 Token 解析为 DOM 节点,并将 DOM 节点添加到 DOM 树中 HTML 解析器维护了一个 Token 栈结构,该 Token 栈主要用来计算节点之间的父子关系,在第一个阶段中生成的 Token 会被按照顺序压到这个栈中。
* 如果压入到栈中的是 StartTag Token,HTML 解析器会为该 Token 创建一个 DOM 节点,然后将该节点加入到 DOM 树中,它的父节点就是栈中相邻的那个元素生成的节点。
* 如果分词器解析出来是文本 Token,那么会生成一个文本节点,然后将该节点加入到 DOM 树中,文本 Token 是不需要压入到栈中,它的父节点就是当前栈顶 Token 所对应的 DOM 节点。
* 如果分词器解析出来的是 EndTag 标签,比如是 EndTag div,HTML 解析器会查看 Token 栈顶的元素是否是 StarTag div,如果是,就将 StartTag div 从栈中弹出,表示该 div 元素解析完成。

HTML 解析器开始工作时,会默认创建了一个根为 document 的空 DOM 结构,同时会将一个 StartTag document 的 Token 压入栈底。

如果解析到 script 标签时,渲染引擎判断这是一段脚本,此时 HTML 解析器就会暂停 DOM 的解析,因为接下来的 JavaScript 可能要修改当前已经生成的 DOM 结构。 JavaScript 文件的下载过程会阻塞 DOM 解析。(可以使用async和defer来优化)脚本执行完成之后,HTML 解析器恢复解析过程,继续解析后续的内容,直至生成最终的 DOM。

如果代码里引用了外部的 CSS 文件,那么在执行 JavaScript 之前,还需要等待外部的 CSS 文件下载完成,并解析生成 CSSOM 对象之后,才能执行 JavaScript 脚本。

不过 Chrome 浏览器做了很多优化,其中一个主要的优化是预解析操作。当渲染引擎收到字节流之后,会开启一个预解析线程,用来分析 HTML 文件中包含的 JavaScript、CSS 等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件。

输出内容:DOM 树。

样式计算

输入内容:DOM 树。

处理过程

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

CSS样式的主要来源:

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

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

CSSOM 的作用:

  • 是提供给 JavaScript 操作样式表的能力;
  • 为布局树的合成提供基础的样式信息。
//theme.css
div{ 
    color : coral;
    background-color:black
}
//foo.js
console.log('time.geekbang.org')
<html>
<head>
    <link href="theme.css" rel="stylesheet">
</head>
<body>
    <div>geekbang com</div>
    <script src='foo.js'></script>
    <div>geekbang com</div>
</body>
</html>

20250622-210950.png 接收到 HTML 数据之后的预解析过程中,HTML 预解析器识别出来了有 CSS 文件和 JavaScript 文件需要下载,然后就同时发起这两个文件的下载请求,需要注意的是,这两个文件的下载过程是重叠的,所以下载时间按照最久的那个文件来算。

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

需要将所有值转换为渲染引擎容易理解的、标准化的计算值

20250622-184608.png

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

根据 CSS 的继承规则和层叠规则计算DOM树中每个节点的样式属性,带有完整样式的 DOM 树(Styled DOM Tree)。

输出内容:完整样式的 DOM 树(Styled DOM Tree)。

布局阶段

计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局。

输入内容:完整样式的 DOM 树(Styled DOM Tree)。

处理过程

1. 创建布局树
  • 遍历 DOM 树中的所有可见节点(html标签属于可见节点),并把这些节点加到布局树中;
  • 而不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容,再比如 body.p.span 这个元素,因为它的属性包含 dispaly:none,所以这个元素也没有被包进布局树。
2. 布局计算

输出内容:布局树。

分层

渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树。 可打开 Chrome 的“开发者工具”,选择“Layers”标签,就可以可视化页面的分层情况。 浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。

输入内容:布局树。

处理过程: 并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。

20250622-194212.png 元素满足以下任意一点,就会被提升成为单独一层。

  1. 拥有层叠上下文属性的元素会被提升为单独的一层。 层叠上下文:控制元素在Z轴(垂直于屏幕方向)堆叠顺序的核心机制。
  2. 需要剪裁(clip)的地方也会被创建为图层(如果出现滚动条,滚动条也会被提升为单独的层。)。

分层是从宏观上提升了渲染效率。

will-change
.box {
    will-change: transform, opacity;
}

这段代码就是提前告诉渲染引擎 box 元素将要做几何变换和透明度变换操作,这时候渲染引擎会将该元素单独实现一层,等这些变换发生时,渲染引擎会通过合成线程直接去处理变换,这些变换并没有涉及到主线程,这样就大大提升了渲染的效率。这也是 CSS 动画比 JavaScript 动画高效的原因

如果涉及到一些可以使用合成线程来处理 CSS 特效或者动画的情况,就尽量使用 will-change 来提前告诉渲染引擎,让它为该元素准备独立的层。但是凡事都有两面性,每当渲染引擎为一个元素准备一个独立层的时候,它占用的内存也会大大增加,因为从层树开始,后续每个阶段都会多一个层结构,这些都需要额外的内存,所以你需要恰当地使用 will-change。

输出内容:图层树。

绘制

输入内容:图层树。

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

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

输出内容:绘制列表。

分块

合成线程会将每个图层分割为大小固定的图块,然后优先绘制靠近视口的图块,这样就可以大大加速页面的显示速度。合成线程会按照视口附近的图块来优先生成位图。

不过有时候, 即使只绘制那些优先级最高的图块,也要耗费不少的时间,因为涉及到一个很关键的因素——纹理上传,这是因为从计算机内存上传到 GPU 内存的操作会比较慢。

为了解决这个问题,Chrome 又采取了一个策略:在首次合成图块的时候使用一个低分辨率的图片。比如可以是正常分辨率的一半,分辨率减少一半,纹理就减少了四分之三。在首次显示页面内容的时候,将这个低分辨率的图片显示出来,然后合成器继续绘制正常比例的网页内容,当正常比例的网页内容绘制完成后,再替换掉当前显示的低分辨率内容。

20250622-200648.png

光栅化

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

20250622-200849.png

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

20250622-200928.png

合成

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

20250622-201112.png

三个和渲染流水线相关的概念

重排

20250622-201344.png 改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排,重排需要更新完整的渲染流水线,所以开销也是最大的。

重绘

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

合成

那如果你更改一个既不要布局也不要绘制的属性,渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成。相对于重绘和重排,合成能大大提升绘制效率。 **合成操作是在合成线程上完成的,这也就意味着在执行合成操作时,是不会影响到主线程执行的。**这就是为什么经常主线程卡住了,但是 CSS 动画依然能执行的原因。

20250622-201653.png

文章内容包含个人理解以及大模型回答,仅供参考,请仔细甄别,欢迎随时指正!感谢大家!