浏览器是如何将 HTML、CSS、JavaScript 变成可视的页面的
渲染流程
渲染模块在执行过程中会被划分为很多子阶段 输入的HTML、CSS、JavaScript
经过这些子阶段处理输出像素 这个处理流程叫做渲染流水线
流水线分为以下几个阶段
- 构建
DOM
树 - 样式计算
- 布局阶段
- 分层
- 绘制
- 分块
- 栅格化
- 合成
那既然流程这么复杂 如何避免搞混呢? 其实只需要理解以下三个部分就行 即
- 开始时每个子阶段都有其输入的内容
- 然后每个子阶段都有其处理过程
- 最终每个子阶段会生成输出内容
1.构建 DOM 树
Q: 为什么要构建DOM
树呢
A: 因为浏览器无法直接理解和使用HTML
所以需要将HTML
转换成浏览器能够理解的DOM
树
接下来我们看一下DOM
树的构建过程
从图中可以看出 构建DOM
树的输入内容是一个简单的HTML
文件 经过HTML
解析器解析 最终输出树状结构的DOM
树
至此构建DOM
树的阶段已经完成 接下来便是让DOM
节点拥有正确的样式 这一阶段便是样式计算
2.样式计算
样式计算的目的是计算出DOM
树节点中每个元素的具体样式 可分为三步完成
把
css
转换成浏览器能够理解的结构(styleSheets
)可以看出
CSS
的来源主要有三种- 通过
link
引入的外部CSS
文件 <style>
标记内的CSS
- 元素的 style 属性内嵌的
CSS
当浏览器接收到
CSS
文本时 会执行转换操作 将其转换成浏览器可以理解的styleSheets
结构 该结构同时具有查询和修改功能可以在 chrome 浏览器控制台输入
document.styleSheets
查看- 通过
转换样式表中的属性值 使其标准化
第一步我们已经将
CSS
文本转换成可以理解的styleSheets
结构了 接下来便是对属性值的表转化操作从图中看出
aqua
被解析为rgb(0, 255, 255)
……这就是标准化过程计算出
DOM
树中每个节点的具体样式
计算具体属性涉及到CSS
的继承规则和层叠规则
- 继承:
dom
节点可以继承父节点的一些属性值 - 层叠:定义了如何合并来自多个原的属性值的算法
一句话概括 样式计算阶段实在遵循CSS
的继承和层叠规则下 计算输出每个DOM
节点的样式并保存在ComputedStyle
的结构内(可通过 Chrome 的 Computed 查看)
3.布局阶段
布局阶段主要有两个任务
- 创建布局树
- 遍历
DOM
树中的所有可见节点 并把这些节点添加到布局中 - 不可见的节点会被布局树忽略掉
- 遍历
- 布局计算
4.分层
在完成布局阶段后 依旧不能着手绘制页面 因为为了更加方便地实现一些复杂的效果(比如 3D 变换 页面滚动等)渲染引擎为特定的节点生成专用的图层 并生成一颗对应的图层树 由这些图层叠加在一起构成了最终的页面图像 (可以使用 chrome 的 Layers 功能查看页面的图层构成以及调试图层渲染过程)
元素可以被单独提升为一个图层的情况:
- 拥有层叠上下文属性的元素 如定位属性 透明属性
CSS
滤镜等
2.需要剪裁的地方也会创建图层
例如div
里面的文字较多且超出了显示区域 这个时候就产生了裁剪 渲染引擎会将裁剪文字内容的一部分用于显示在div
区域 出现这种裁剪情况的时候 渲染引擎就会为文字部分单独创建一个层 如果出现滚动条 滚动条也会提升为单独的层
5.图层绘制
完成图层树的构建后 渲染引擎会对图层树中的每个图层进行绘制。
举个例子: 如果我们要画一副五星红旗 我们会怎么操作?通常 我们会进行这样的步骤
- 绘制红色背景
- 画一个大五角星
- 画剩余的四个五角星
其实图层绘制的步骤和这个差不多 渲染引擎会将一个图层的绘制拆分成很多绘制指令 再将这些绘制指令按照顺序组成一个待绘制列表 如下图(可以通过 chrome 的 Layers 中的 document 层查看调试)
6.栅格化操作
绘制列表只是用来记录绘制顺序和绘制指令的列表 而实际上操作是由绘制引擎中的合成线程来完成的(类似于待办事项里的代办只是用来记录的列表 而执行这些事项的是你自己) 那合成线程和渲染主线程之间有啥关系呢
实际上 当图层的绘制列表准备好之后 主线程会将该列表commit
给合成线程 而合成线程会将图层切割成多个图块 按照视口(简单理解为屏幕可视区域)附加的图块来优先生成位图 实际生成位图的操作是由栅格化来执行的。
Q: 为什么要优先将可视区附加的图块生成位图?
A: 当图层很长很大的时候 用户不一定会查看所有区域 如果一次性绘制所有图层的话 会产生太大的开销 也没有必要
Q: 什么是栅格化?
A: 将图块转化为位图(图块是栅格化的最小单位)
渲染进程维护了一个栅格化的线程池 所有图块栅格化都是在线程池内执行的
栅格化通常会使用 GPU 来加速生成 使用 GPU 生成位图的过程称为快速栅格化 生成的位图被保存在 GPU 内存中
7.合成和显示
一旦所有图块被栅格化 合成线程就会生成一个绘制图块的命令——“DrawQuad” 然后提交该命令给浏览器进程
浏览器进程中的viz组件 根据DrawQuad命令 将页面内容绘制到内存中 然后将内存显示在屏幕上
到这里 经过一系列操作 HTML CSS JavaScript 文件就会变成屏幕可视的页面了。
8.总结
一个完整的渲染流程简述如下
- 渲染进程将
HTML
内容转换为能够读懂的DOM
树结构 - 渲染引擎将
CSS
样式表转换为styleSheets
并计算出DOM
节点的具体样式 - 创建布局树 计算元素的布局信息
- 对布局树进行分层 生成分层树
- 为每个图层生成绘制列表 并将其提交到合成进程
- 合成进程将图层分成图块 并在栅格化线程池将其转换为位图
- 合成线程发送绘制图块命令DrawQuad给浏览器进程
- 浏览器根据DrawQuad生成页面并显示到显示器上
相关概念
1.重排
当修改了元素的几何属性(宽度 高度等)会使浏览器重新布局 解析之后的一系列子阶段 就叫重排 重排后需要更新完整的渲染流水线 所以开销是最大的
重排一定会导致重绘
2.重绘
更改元素的背景色等 (不会触发布局阶段 没有引起集合位置的变化 )直接进入了绘制阶段 然后执行之后的一系列子操作 就叫重绘
Q:重排快还是重绘快?
A:重绘省去了布局和分层阶段 所以执行效率更快
重绘不一定导致重排
3.直接合成阶段
当做了既不导致重排 也不导致重绘的操作 就跳过布局和绘制 只执行后续操作 称为直接合成
例如 transform
可以避免重排和重回 直接在非主线程上执行合成动画操作(这也是效率最高的 因为没有占用主线程的资源 也没有重排和重绘)
Q: 如何减少重排和重绘?
A: 触发重排和重绘的操作尽量放在一起(如修改高度或者边距) 通过虚拟dom 层计算出操作总得差异 一起提交给浏览器(例如使用createdocumentfragment来汇总append的dom
来减少触发重排重绘次数)
结语:相信看了上面的文章 大家对于浏览器是如何将 HTML CSS JavaScript 文件处理并生成我们看到的页面这个过程有了更深的理解 文章若有错误之处 请大家在评论区指出指正!笔者最近正在学习浏览器的工作原理及实践的相关内容 感兴趣的小伙伴可以点波关注一起学习 后续会继续更新相关系列内容!