浏览器渲染原理
什么是渲染?
渲染(render),简单来说就是浏览器将 HTML、CSS、JavaScript 等代码,转化为用户能直观看到的可视化网页界面的全过程。
更细一点讲,就是根据 HTML、CSS、JavaScript 等代码,计算浏览器视口区域每个像素点的显示颜色(浏览器牛皮),光想想都知道这个过程时极其复杂的。
渲染的时机
在浏览器地址栏输入网页地址回车之后,浏览器做了些什么?
这是一个很常见的面试题。我们现在就通过这道面试来分析了解浏览器渲染页面的时机。
简单来说就是做了两件事, 拿 HTML (网络线程)和 渲染页面 (渲染主线程)
这篇文章主要是写渲染相关的,网络线程我就稍微简单的一笔带过了
网络线程:解析输入的地址(DNS 域名解析)、建立 TCP 连接、发送 HTTP 请求(请求成功就能拿到 HTML 了)
当网络线程收到 HTML 文档之后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列,在事件循环机制的作用下,渲染主线程会取出并执行渲染任务,开启渲染流程(我的另一篇文章细讲了事件循环相关的知识)。
渲染过程
整个页面渲染的流程分为多个阶段,分别是:
HTML 解析、样式计算、布局、分层、绘制、分块、光栅化、画
每一个阶段都有明确的输入和输出,上一个阶段的输出会成为下一个阶段的输入。
下面我们来逐一了解这些渲染的阶段。
解析 HTML
首先的第一个阶段就是解析HTML,生成 DOM 和 CSSOM,我们拿到的HTML文档其实就是一个字符串,为了后续的操作和计算需要通过 HTML 和 CSS 代码生成对象结构的树。
生成 DOM 树
这个阶段首先会根据 HTML 文档,生成 DOM (Document Object Model) 树,DOM 树 是文档对象模型(Document Object Model) 的树状表现形式,它是浏览器解析 HTML 文档后,生成的一套跨平台、语言无关的编程接口,本质是把 HTML 里的所有内容(元素、属性、文本等)映射成一个个节点,再按 HTML 的嵌套关系组织成树状的对象结构(对象结构相比字符串更便于操作)。
比如我们这里有一段 HTML 代码:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>DOM & CSSOM 示例</title>
<style>
/* 标签选择器 */
body {
background-color: #f5f5f5;
font-size: 16px;
}
/* 类选择器 */
.content-box {
color: #333;
padding: 20px;
}
/* ID 选择器 */
#title {
font-weight: bold;
color: #0066cc;
}
</style>
</head>
<body>
<div class="content-box">
<h1 id="title">DOM & CSSOM 演示</h1>
<p>这是一段测试文本</p>
</div>
</body>
</html>
对应的 DOM 树结构就是下面这样的(懒得画图了,将就一下)
html(根节点,属性:lang="zh-CN")
├── head(子节点)
│ ├── meta(子节点,属性:charset="UTF-8")
│ ├── title(子节点)
│ │ └── 文本节点:"DOM & CSSOM 示例"
│ └── style(子节点,内容为 CSS 代码,仅作为文本存储,不解析)
└── body(子节点)
└── div(子节点,属性:class="content-box")
├── h1(子节点,属性:id="title")
│ └── 文本节点:"DOM & CSSOM 演示"
└── p(子节点)
└── 文本节点:"这是一段测试文本"
遇到 CSS 代码
主线程当解析 HTML 的过程中解析到 <link rel="stylesheet"> (外部资源引用),会触发 CSS 解析器(预解析线程),去加载对应的 CSS 代码,然后交给渲染主线程生成 CSSOM(CSS Object Model)树。
渲染主线程解析 HTML 时,遇到 <style> 标签,会先把 <style> 标签本身解析成 DOM 节点;然后读取标签内的 CSS 文本并解析生成 CSSOM(CSS Object Model)树。
每个样式表都会生成对应的 CSSOM,并放在 StyleSheetList 对象下面。
上面 HTML 解析出来的 CSSOM 结构(一个样式表的)如下(依旧将就):
CSSOM 根(隐含,包含所有样式规则)
├── 选择器:body
│ ├── background-color: #f5f5f5
│ └── font-size: 16px
├── 选择器:.content-box
│ ├── color: #333
│ └── padding: 20px
├── 选择器:#title
│ ├── font-weight: bold
│ └── color: #0066cc (优先级高于 .content-box 的 color,最终生效)
遇到 JS 代码
如果主线程解析到 script 标签,会停止解析HTML,转而等待 JS 文件下载完成,并将全局代码解析执行完成之后,才能继续解析 HTML。
HTML 的解析会生成 DOM 和 CSSOM,再根据它们去渲染页面,JS 代码执行则有可能会修改当前的 DOM 和 CSSOM,所以HTML的解析需要暂停,这就是 JS 阻塞 HTML 解析的根本原因。
样式计算
这一步会遍历 HTML 解析生成的 DOM 树的每一个节点进行样式计算,生成一个包含最终样式(Computed Style)的 DOM 树。
我们可以在开发者工具中的 Computed 里面查看选中元素的 Computed Style,里面包含了元素所有样式(不止是我们设置的)。

样式计算的过程也是很复杂的,后续看看再写一篇文章讲一下吧。
布局
这一步依次遍历包含最终样式(Computed Style)的 DOM 树,计算每一个节点元素的 尺寸 和 位置(相对于包含块) ,生成一个 Layout 树(C++对象)。
很多情况下DOM 树 和 Layout 树的节点不是一一对应的:
- 一些隐藏的节点(
display: none)没有几何信息(尺寸和位置),Layout 树就不会有这个节点。 - 如果有伪元素, Layout 树则会有多的元素节点。
- 由于有 内容必须在行盒中 的规则,Layout 树中块盒文本外会有一层匿名行盒。
- ......
JS 可以通过布局属性拿到一些布局树的属性。
| API / 属性 | 作用 |
|---|---|
element.getBoundingClientRect() |
返回元素的边界矩形(left/top/right/bottom/width/height) |
element.offsetWidth/offsetHeight |
返回元素的布局宽度 / 高度 |
element.offsetLeft/offsetTop |
返回元素相对于 offsetParent 的偏移量 |
element.clientWidth/element.clientHeight |
返回元素的内容区宽度 / 高度 |
分层
这一步浏览器主线程会对布局树进行分层,如果某一个层改变后,仅会对该层进行后续处理,不影响其他层。
滚动条、堆叠上下文、transform、opacity 等样式都会影响分层结果,也是以使用 will-change属性更大程度的影响分层结果。
我们可以再开发者工具的 Layers 中查看页面的分层信息
绘制
这一步浏览器主线程会为每一个层生成绘制指令集,用于描述这一层的内容如何绘制。
主线程的渲染工作到这一步就结束了,后续的步骤会交给其他线程完成。
分块
完成绘制步骤后,主线程会将每一个图层的绘制信息提交给合成线程,剩余工作将交由合成线程完成。
合成线程首先会将每个图层分块,将其划分为更多的小区域。
合成线程会从线程池拿取多个线程来完成分块工作。
光栅化
分块完成之后就会进入光栅化阶段,合成线程会将分块信息交给 GPU 进程完成光栅化。
GPU 进程会开启多个线程来完成光栅化,这个阶段会优先处理靠近视口区域的分块。
光栅化的结果就是每个块的位图(像素点颜色信息)
画
这个阶段合成线程拿到每个层、每个块的位图后,生成一个个「指引(quad)」信息。
指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。因此, transform 变形的操作是合成线程处理,与渲染主线程无关,这就是 transform 效率高的本质原因。
合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件,完成最终的屏幕成像。
Reflow(回流 / 重排)
当我们修改 DOM 元素的几何属性(位置、尺寸等)时,我们实际修改的是 CSSOM ,如果 DOM 或者 CSSOM变化了,就会重新进行样式计算生成 Layout 布局树,再将元素绘制到页面上,这个过程就是 Reflow。
它是浏览器渲染流水线中开销较高的步骤,频繁触发会显著降低页面性能,甚至造成卡顿。
为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当 JS 代码全部完成后再进行统一计算。所以,改动属性造成的 reflow 是异步完成的。
也同样因为如此,当 JS 获取布局属性时,就可能造成无法获取到最新的布局信息。
所以我们获取几何信息属性时,浏览器会立即 Reflow。
Repaint(重绘)
Repaint(重绘) 是浏览器渲染流水线中紧随回流(Reflow)的核心步骤,指当 DOM 元素的视觉样式属性 发生变化(不影响几何位置和尺寸)时,浏览器仅重新为元素绘制像素内容(如颜色、背景、阴影),无需重新计算布局的过程。
Repaint的本质就是重新根据分层信息计算了绘制指令。
当改动了可见样式后,就需要重新计算,会引发 repaint。
由于元素的布局信息也属于可见样式,所以 Reflow 一定会引起 Repaint。