浏览器渲染原理
什么是渲染
浏览器中的 “渲染” 指的是将 HTML 字符串转化为屏幕上的像素信息的过程
。
渲染在什么时候发生
当我们在浏览器键入一个 URL 时,网络线程会通过网络通信拿到 HTML
,但网络线程自身并不会处理 HTML,它会将其生成一个渲染任务
交给消息队列
,在合适的时机
渲染主线程会从消息队列
中取出渲染任务执行
,启动渲染流程
。
渲染流水线
接下来我们重点来讲解渲染的流程,整个过程如下图:
1. 解析 HTML - Parse
由于字符串难以进行操作,浏览器首先会将 HTML 字符串解析成 DOM 树和 CSSOM 树这种容易操作的对象结构,也提供了 js 操作这两棵树的能力。
- HTML 解析过程遇到 CSS
为了提高解析效率,浏览器在开始解析前,会启动一个预解析的线程
,率先下载 HTML 中的外部 CSS 文件和 外部的 JS 文件
。 如果主线程解析到 link 位置,此时外部的 CSS 文件还没有下载解析好,主线程不会等待,继续解析后续的 HTML,这是因为下载和解析 CSS 的工作是在预解析线程中进行的,这就是 CSS 不会阻塞 HTML 解析
的根本原因。
- 主线程解析到 script 位置
如果主线程解析到 Script 位置,会停止解析 HTML
,转而等待 JS 文件下载
好,并将全局代码解析执行完成
后,才能继续解析 HTML
。
提示
这是因为 JS 代码的执行过程可能会修改当前的 DOM 树
,所以 DOM 树的生成必须暂停,这就是 JS 会阻塞 HTML 解析的根本原因。
2. 样式计算 - Computed Style
经过 HTML 解析过后,我们拿到了 DOM 树和 CSSOM 树,但是光得到这两颗树还不够,还需要知道每个 DOM 对应哪些样式。
主线程会遍历得到的 DOM 树,依次为树中的每个节点计算出它最终的样式
,称之为 Computed Style
。在这一过程中,很多预设值会变成绝对值,比如 red 会变成 rgb(255,0,0)
;相对单位会变成绝对单位,比如 em 会变成 px,这一步完成后,会得到一棵带有样式的 DOM 树
。
3. 布局 - Layout
布局阶段会依次遍历
DOM 树的每一个节点,计算每个节点的几何信息
,例如节点的宽高、相对包含块的位置。
大部分时候,DOM 树和布局树并非一一对应:比如 display:none 的节点没有几何信息,因此不会生成到布局树。
又比如使用了伪元素选择器,虽然 DOM 树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中;还有匿名行盒、匿名块盒等等都会导致 DOM 树和布局树无法一一对应。
4. 分层 - Layer
经过布局,每个元素的位置和大小就有了,那下面是不是就该开始绘制页面了?
提示
答案是否定的,因为页面上可能有很多复杂的场景,比如 3D 变化、页面滚动、使用 z-index 进行 z 轴的排序等。所以,为了实现这些效果,渲染引擎
还需要为特定的节点生成专用的图层
,并生成一棵对应的图层树
。
那什么是图层呢?相我们可以在 Chrome 浏览器的开发者工具中,选择 Layers 标签,就可以看到页面的分层情况,以掘金首页为例,其分层情况如下:
可以看到,渲染引擎
给页面分了很多图层,这些图层会按照一定顺序叠加
在一起,就形成了最终的页面
。
- 将页面分解成多个图层的操作就成为
分层
, - 将这些图层合并到一层的操作就成为
合成
。
分层和合成通常是一起使用的。Chrome 引入了分层和合成的机制就是为了提升每帧的渲染效率。 分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率,滚动条、堆叠上下文、transform、opacity 等样式都会或多或少的影响分层结果。
5. 绘制 - Paint
主线程会为每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来。
什么是绘制指令集?
类似于:
- 步骤 1. 将笔移动到(10,30)的位置
- 步骤 2. 画一个 20 * 30 的矩形
- 步骤 3. 将矩形填充为红色
- 步骤 4. ...
渲染引擎在绘制图层
时,会把一个图层的绘制分成很多绘制指令
,然后把这些指令按照顺序组成一个待绘制的列表
:
可以看到,绘制列表中的指令就是一系列的绘制操作。通常情况下,绘制一个元素
需要执行多条绘制指令
,因为每个元素的背景、边框等属性都需要单独的指令进行绘制,所以在图层绘制阶段
,输出的内容就是绘制列表。
在 Chrome 浏览器的开发者工具中,通过 Layer 标签可以看到图层的绘制列表和绘制过程:
绘制列表只是用来记录绘制顺序和绘制指令的列表,而绘制操作是由渲染引擎中的合成线程
来完成的。当图层绘制列表准备好之后,主线程会把该绘制列表提交给合成线程。
6. 分块 - Compositing
合成线程首先对每个图层进行分块,将其划分为更多的小区域,它会从线程池中拿取多个线程来完成分块工作。
7. 光栅化 - Raster
光栅化是将每个块变成位图,位图可以理解成内存里的一个二维数组,这个二维数组记录了每个像素点信息。
合成线程会将块信息交给 GPU 进程,以极高的速度完成光栅化。
GPU 进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。
光栅化的结果,就是一块一块的位图。
8. 呈现 - Draw
经过以上步骤,来到了最终阶段
- 合成线程拿到每个层、每个块的位图后,生成一个个
指引(quad)
信息。 - 指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形
- 变形发生在合成线程,与渲染主线程无关,这就是 transform 效率高的本质原因
- 合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件,完成最终的屏幕成像
总结
回顾一下 浏览器渲染的完整流程
参考
- 从「浏览器进程模型」到「浏览器渲染原理」——前端掘金者 H