这是我参与更文挑战的第20天,活动详情查看: 更文挑战
前言
上一篇文章 已经详细介绍了浏览器的 导航流程,即 从用户发出 URL 请求到页面开始解析的过程,那么接下来将详细介绍浏览器的 渲染过程,即 从页面开始解析到页面完整展示的过程。
我们把 HTML、CSS、JavaScript 等文件交给浏览器,经过浏览器内部的一些处理就可以显示出漂亮的页面,那在探究浏览器内部怎么处理之前,先回顾一下 HTML、CSS、JavaScript 是什么:
- HTML:HTML 的内容是由标签和文本组成。每个标签都有它自己的语义,浏览器会根据标签的语义来正确展示 HTML 内容。
- CSS:即层叠样式表,由选择器和属性组成,如果需要改变 HTML 的字体颜色、大小等信息,就会用到 CSS。
- JavaScript:简称 JS,通过 JS 可以修改页面内容,让页面“动”起来。
这也是 Web 标准的三大组成部分:结构、表现和行为。
浏览器把 HTML、CSS、JS 渲染成页面是很复杂的一个过程,所以在渲染过程中会划分为多个子阶段,按照时间顺序可以划分为如下几个阶段:构建 DOM 树、样式计算(解析 CSS)、获取布局树、生成图层树、图层绘制、栅格化处理、合成显示。
下面将逐一介绍这些子阶段,每个阶段都有 获取输入内容、处理内容、生成输出内容 这样的周期存在,关注这些内容会更加清晰地了解每个子阶段。
构建 DOM 树
浏览器是没有办法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树。DOM 把文档作为一个树形结构,树的每个结点表示了一个 HTML 标签或标签内的文本项。
DOM 提供了对 HTML 文档结构化的表述。在渲染引擎中,DOM 有三个层面的作用:
- 从页面的视角来看,DOM 是生成页面的基础数据结构。
- 从 JavaScript 脚本视角来看,DOM 提供给 JavaScript 脚本操作的接口,通过这套接口,JavaScript 可以对 DOM 结构进行访问,从而改变文档的结构、样式和内容。
- 从安全视角来看,DOM 是一道安全防护线,一些不安全的内容会在 DOM 解析阶段被拒之门外。
简而言之,DOM 是表述 HTML 的内部数据结构,它会将 Web 页面和 JavaScript 脚本连接起来,并过滤一些不安全的内容。
构建 DOM 树的过程:
- 输入:从网络请求或者缓存中拿到的 HTML 文件
- 处理:通过 HTML 解析器解析生成 DOM 树,大概过程就是:拿到 HTML 文件的字节流数据,将这些字节数据 转化为字符串,然后将这些字符串转化为 标记(
token
),然后把这些标记进行 词法分析 转化为DOM
节点,再将DOM
节点 构建为一棵 DOM 树。 - 输出:输出一个树状结构的 DOM,即 DOM 树。
DOM 和 HTML 内容几乎是一样的,但是和 HTML 不同的是,DOM 是保存在内存中树状结构,可以通过 JavaScript 来查询或修改其内容。
如下,我们可以通过打印 document
或在 Elements
查看 DOM,下面是一个简单的 DOM 树示例:
样式计算
样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式。
和 HTML 一样,渲染引擎也是无法直接理解 CSS 文件内容的,所以渲染引擎会先将其解析成 浏览器可以理解的结构,这个结构就是 styleSheets
。网上有很多地方是说把 CSS 文件转换为 CSSOM 树的,而在 Chromium 源码(需要翻墙) 上并没有 CSSOM 这个词,这个 styleSheets
是能够直观感受得到的。不过名词是什么并不重要,我也喜欢 CSSOM 这个词,重要的是我们要理解这个 样式计算 的过程。
我们可以通过 document.styleSheets
查看 styleSheets
结构:
样式计算的过程:
- 输入:CSS 样式来源主要有三种:
- 通过
link
引用的外部 CSS 文件 <style>
标签内的 CSS- 元素的
style
属性内嵌的 CSS
- 通过
- 处理:
- 把 CSS 转换为浏览器能够理解的结构–
styleSheets
,styleSheets
具有两个作用:- 第一个是 提供给 JavaScript 操作样式表的能力;
- 第二个是 为布局树的合成提供基础的样式信息。
- 转换样式表中的属性值,使其标准化;即需要把所有的值转换为渲染引擎容易理解的、标准化的计算值
- 例如:把
2em
转换为32px
、把blue
转换为rgb(0, 0, 255)
、把font-weight: bold
转换为font-weight: 700
- 例如:把
- 计算出 DOM 树中每个节点的具体样式,在这个过程中需要遵守 CSS 的继承规则和层叠规则
- CSS 继承就是每个 DOM 节点都包含有父节点的样式
- 层叠是 CSS 的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。
- 把 CSS 转换为浏览器能够理解的结构–
- 输出:每个 DOM 节点的样式,并被保存在
ComputedStyle
的结构。
可以按 F12
打开开发者工具随便选择一个 Elements
元素,再选择 Computed
标签,如下图,右下角红色框内便是选中标签最终的 ComputedStyle
的值,了解了这个,平时改内置组件的样式会不会方便一些呢~
获取布局树
现在已经有了 DOM 树和 DOM 树中每个元素的样式,但还不能够渲染一个完整的页面,因为浏览器并不知道这些元素在页面上的 几何位置。所以接下来就是 计算出 DOM 树中可见元素的几何位置,也就是布局(Layout)。
布局阶段:
- 输入:DOM 树和 CSSOM 树
- 处理:
- 构建一棵只包含可见元素的布局树
- 遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中
- 而不可见的节点会被布局树忽略掉,如属性包含
dispaly:none
的元素不会被包进布局树
- 布局计算:计算布局树节点的坐标位置,然后写回布局树中
- 构建一棵只包含可见元素的布局树
- 输出:包含 DOM 元素样式和位置的布局树
如下图(来自 渲染流程(上):HTML、CSS和JavaScript,是如何变成页面的?):
生成图层树
得到 布局树 之后要对其进行 分层,就好像搞定了它的 x
、y
轴,之后是处理的是它的 z
轴,在 CSS 上比较能体现的就是 z-index
z
轴排序属性、position
定位属性、opacity
透明属性等。通过分层可以实现一些复杂的效果,比如 3D 变换、页面滚动等,那么为了更加方便的实现这些效果,渲染引擎会为特定的节点生成专用的图层,并且生成一棵对应的图层树(LayerTree
)。如果了解过 Photoshop
,相信会更加容易理解图层这个概念。
当然 并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。要满足以下两个条件中的一个,渲染引擎才会为特定的节点创建新的图层:
- 第一点,拥有 层叠上下文属性 的元素会被提升为单独的一层。
- 即明确定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素等拥有层叠上下文属性的元素
- 第二点,需要裁剪的地方 也会被创建为图层。
- 需要裁剪即那些内容超出指定区域的,如一个
div
它的大小为200 * 200
像素,当里面的文字内容过多超出200 * 200
的面积时会裁剪,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层。
- 需要裁剪即那些内容超出指定区域的,如一个
这个图层分层的情况可以打开 Chrome 的开发者工具 Layers
查看,默认展示的工具中是没有的,它在右上角 ...
更多中的 More tools
里面,它有一些如平移、旋转、复位的操作,见下图:
生成图层树 的过程:
- 输入:布局树
- 处理:将特定节点生成专用图层
- 拥有 层叠上下文属性 的元素会被提升为单独的一层
- 需要裁剪的地方 会被创建为图层
- 输出:图层树
图层绘制
在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行 绘制,它会把一个图层的绘制拆分成很多小的 绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。
绘制列表中的指令其实非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。所以在图层绘制阶段,输出的内容就是这些待绘制列表。
如下图,打开 Chrome 的开发者工具中的 Layers
工具,选择 document
层,可以来实际体验下绘制列表,左边的区域是 document
的绘制列表,拖动右边区域中的进度条可以重现列表的绘制过程。
图层绘制 的过程:
- 输入:图层树
- 处理:
- 渲染引擎对图层树中每个图层进行绘制
- 拆分成绘制指令,生成绘制列表,提交到合成线程
- 输出:绘制列表
栅格化处理
绘制列表 只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的 合成线程 来完成的。
当图层的绘制列表准备好之后,主线程 会把该绘制列表提交给 合成线程。通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做 视口(viewport
)。在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。
基于这个原因,合成线程会将图层划分为图块(tile
),这些图块的大小通常是 256x256
或者 512x512
。
合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的。
通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫 快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。
栅格化处理 的过程:
- 输入:绘制列表
- 处理:
- 根据视口,合成线程会将图层划分为 图块
- 合成线程会按照视口附近的图块来优先生成 位图
- 输出:保存在 GPU 内存中的位图
合成显示
- 一旦所有图块都被栅格化,合成线程 就会生成一个绘制图块的命令——
DrawQuad
,然后将该命令提交给 浏览器进程 - 浏览器进程里面有一个叫
viz
的组件,用来接收合成线程发过来的DrawQuad
命令,然后将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
这样到显示阶段,页面就加载显示完成了,这就是整个过程了。
如下图(来自 渲染流程(下):HTML、CSS和JavaScript,是如何变成页面的?):
渲染流程总结
- 浏览器不能直接理解 HTML 数据,所以第一步是 渲染进程将其转换为浏览器能够理解的 DOM 树结构
- 生成 DOM 树后,渲染引擎将 CSS 样式表转化为浏览器可以理解的
styleSheets
,计算出 DOM 节点的样式 - 创建布局树,并计算 DOM 元素的布局信息,使其都保存在 布局树 中
- 对布局树进行分层,并生成 图层树
- 为每个图层生成 绘制列表,并将其提交到合成线程
- 合成线程 将图层分成 图块,并在光栅化线程池中将图块转换成 位图
- 合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令消息生成页面,并显示到显示器上
以上就是 宏观视角 下浏览器的渲染流程了,参考了极客时间李兵老师的 《浏览器工作原理与实践》,这是一个非常棒的专栏,推荐阅读。