页面渲染是浏览器的核心功能之一,所谓渲染流程,本质就是将 HTML 字符串转换为屏幕上像素点 的过程。
这个过程看似简单,实则是一套由多进程多线程协同完成的复杂流水线。
渲染时机
在探讨整个渲染流程之前,我们需要先明确渲染是怎样开始的。
通常在用户访问某个网址时,浏览器就会发起一个网络请求,向目标服务器请求对应页面。
当浏览器的网络线程 收到 HTML 文档后,会产生一个渲染任务 ,并将其传递给渲染主线程的消息队列。
在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启渲染流程。

渲染流程总览
整个渲染流程分为多个阶段,分别是:HTML 解析 、样式计算 、布局 、分层 、绘制 、分块 、光栅化 、Draw。
每个阶段都有明确的输入输出,上一个阶段的输出会成为下一个阶段的输入。

各个阶段环环相扣,最终形成了一套组织严密的生产流水线,任何一个环节的阻塞 或延迟 都会影响最终的渲染性能。
1. HTML 解析
Parse HTML 是渲染的第一步,这一步的输入是通过网络请求拿到的 HTML 字符串。
浏览器在开始解析前,会启动一个预解析线程 ,率先下载 HTML 中的外部 CSS 文件 和外部 JS 文件。
最终,HTML 字符串会被逐行解析为 DOM 树 ,CSS 资源加载并解析后生成 CSSOM 树 ,这两个树形结构对象也赋予了 JS 操纵页面 DOM 和样式的能力,它们也是该阶段的最终输出。
两棵树上的节点并非 JS 对象,而是由浏览器内核实现的 C++ 对象
JS 能操作的是浏览器提供的这些节点对象的"代理对象"

CSSOM 树 内包含了所有的浏览器默认样式、内部样式、外部样式、行内样式,平时我们通过dom.style.xxx
操作的只是元素的行内样式,不会影响开发者定义的内部和外部样式表。
想要访问除内联样式和浏览器默认样式外的内部和外部样式表,我们可以通过document.styleSheets
接口,它是 JS 访问 CSSOM 的核心接口之一,提供了操作 CSSOM 中样式规则的方法。
document.styleSheets
访问的只是 CSSOM 的映射,而不是其本身

💎 HTML 解析过程中遇到 CSS 代码怎么办?
如果主线程解析到<link>
位置,此时外部的 CSS 文件还没下载解析好,渲染主线程不会等待 ,而是会继续解析后续的 HTML。
这是因为下载和解析 CSS 的工作是在预解析线程 中进行的,这也是 CSS 不会阻塞 HTML 解析的根本原因。
❗ 虽然 CSS 解析不会阻塞 HTML 解析,但由于还没有 CSSOM 生成,仍会阻塞后续的渲染。
预解析线程并不会负责 CSS 的所有解析工作,渲染主线程接手后会完成最后的部分并生成 CSSOM

💎 HTML 解析过程中遇到 JS 代码怎么办?
渲染主线程解析到<script>
位置会暂停一切行为 ,转而等待 JS 文件下载好,并将全局代码解析执行完成后,才能继续解析 HTML。
这是因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以 DOM 的生成必须暂停,这也是 JS 会阻塞 HTML 解析的根本原因。

2. 样式计算
样式计算阶段的输入是 HTML 解析后得到的 DOM 树和 CSSOM 树。
在本阶段,主线程会遍历 得到的 DOM 树,结合 CSSOM 树,依次为树中的每个节点计算出它最终的样式 (Computed Style),以完成 CSS 属性值的计算过程。
这一过程中,很多预设值 会变成绝对值 ,比如red
变为rbg(255,0,0)
;相对单位 变为绝对单位 ,比如em
会变为px
。
可以通过 getComputedStyle 方法获取 DOM 元素的最终样式,其值就是该阶段的计算结果
这一步完成后,就会输出一颗带有样式的 DOM 树。有了这棵树,现在我们就可以清晰地知道每个节点"应该长什么样子",但还不知道"应该放在哪里"。

3. 布局
布局阶段就是来解决"放在哪里"的问题,它的输入是样式计算完成后得到的带样式的 DOM 树。
本阶段会依次遍历 DOM 树的每个节点,计算节点的几何信息 ,比如节点的宽高、包含块的位置等等,这步完成后,最终会得到一棵 Layout 树。

JS 并没有提供访问布局树映射的接口
虽然获取不到整个节点对象,不过我们访问的dom.clientWidth
、dom.offsetWidth
等就是布局树的信息
DOM 树和 Layout 树不一定 是一一对应的,因为布局树展示的是元素的几何信息 ,而有些元素因为各种各样的 CSS 样式,最终导致页面的呈现和 DOM 结构不一致。比如:
有的元素设置了display: none
,就不会在页面上有几何信息。

有的元素设置了伪元素,页面上会多伪元素的几何信息。

还要考虑匿名行盒等。

4. 分层
渲染主线程会使用一套复杂的策略对整个 Layout 树进行分层。
分层的核心价值 在于实现 "局部渲染隔离 ",当某一图层内容发生变化时,浏览器只需针对该图层执行后续的绘制、光栅化等操作,无需牵动整个页面,这也是现代浏览器优化渲染性能的重要手段。
这一过程中,滚动条 的存在、堆叠上下文 的形成,以及transform
、opacity
等样式的应用,都会成为影响分层决策 的关键因素 ------ 它们会通过改变元素的视觉层级关系或渲染特性,促使浏览器将相关元素分离为独立图层。
此外,开发者还可以通过will-change
属性主动 向浏览器传递优化提示(如will-change: transform
),这一属性能更直接地影响分层逻辑,让浏览器提前为可能发生变化的元素创建独立图层,从而为后续的局部更新(如动画效果)做好准备,进一步提升渲染效率。

需要注意的是,分层并不是越多越好,层级过多会导致计算机资源消耗激增
如果 GPU 在合成时需处理成百上千个图层的位置计算,反而拖慢渲染速度
在谷歌浏览器中,可以通过开发者工具查看当前页面的分层。

5. 绘制
这里的绘制,并不是指开始在屏幕上画像素点了,而是指为每一层生成如何绘制的指令集。
这一步会遍历 每个图层的布局信息与样式规则,将抽象的样式属性(如background
、border
、box-shadow
等)转化为具体的绘制操作指令。
这些指令以有序的步骤描述了如何构建图层内容,例如 "先绘制背景色填充整个区域"、"再绘制 1px 的黑色边框"、"最后在指定位置渲染文本内容" 等。
值得注意的是,绘制指令的生成会考虑元素的层叠顺序(如
z-index
)和绘制上下文(如clip-path
的裁剪范围),确保指令集能准确还原样式定义的视觉效果。

这一步完成之后,渲染主线程 的工作就到此为止,后续的步骤会由其他线程完成。
6. 分块
完成绘制之后,渲染主线程会将每个图层的绘制信息 提交给合成线程 (渲染进程中的另外一个线程),剩余工作会由合成线程完成。
合成线程会对每个图层进行分块,将其划分为更多的小区域。
合成线程会从线程池拿取多个线程来完成该工作

"分块"这一步的意义在于,将复杂的渲染任务拆解 成可并行、可增量处理的小单元,来支持局部渲染 和 GPU 加速 等优化手段。比如,一个图层中在视口内的"块"会优先渲染。

7. 光栅化
分块完成后,就会进入光栅化 阶段,这一阶段的核心任务是将合成线程生成的区块绘制指令 ,转化为可直接用于屏幕显示的像素信息,彻底完成了从 "逻辑指令" 到 "物理像素" 的转换。
合成线程会将每个分块的绘制指令(包含该区块的图层归属、绘制步骤、坐标范围等信息)批量提交给 GPU 进程 。结合 GPU 硬件的加速能力,会以极高的效率完成光栅化任务。

合成线程会根据当前页面滚动位置 和可视区域 ,优先向 GPU 提交视口内及邻近区域的块信息。
这意味着用户当前能看到的内容会被优先转换为像素,即使页面其他区域的光栅化尚未完成,也能保证用户快速看到核心内容,这种策略显著提升了页面的加载感知速度。
最后,光栅化阶段的输出,就是一块一块的位图 ,这些位图会暂存在 GPU 的显存中,等待后续画阶段被组合成完整的页面图像。

8. Draw
光栅化完成,合成线程拿到每个层、每个块的位图后,会生成一个个 quad(指引)信息。
每个 quad 会精确描述对应位图的显示参数 ,包括在屏幕坐标系中的位置坐标、尺寸大小,以及是否需要应用旋转、缩放、倾斜等几何变换。
所以变形发生在合成线程,与渲染主线程无关
合成线程会把 quad 信息批量提交给 GPU 进程,由 GPU 进程 产生系统调用,提交给 GPU 硬件,完成最终的屏幕成像。

💎 transform 效率高的原因
transform
的效率显著高于 其他布局属性,是因为transform
的变换逻辑完全在合成线程 中处理,仅体现在 quad 信息的参数调整 ,不会触发渲染主线程的任何工作(如布局计算、样式重算或绘制指令生成),它影响的只是渲染流程的最后一个 Draw 阶段(也称合成绘制阶段)。
相比之下,修改left
、width
等属性会直接触发布局阶段的重排 ,进而引发重绘 和重新光栅化,这些过程都需要渲染主线程参与,成本高昂。
反之,渲染主线程无论如何忙碌,也不会影响transform
的变化。
举一个极端例子,我们同时开启一个left
动画和一个transform
动画,然后让页面进入死循环,页面卡死后transform
动画依然会继续运行,因为它不在渲染主线程工作,而是合成线程。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>动画差异演示</title>
<style>
.box {
width: 100px;
height: 100px;
margin: 20px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
}
.left-box {
background-color: #ef4444;
position: relative;
animation: leftMove 2s linear infinite;
}
.transform-box {
background-color: #3b82f6;
animation: transformMove 2s linear infinite;
}
@keyframes leftMove {
0% {
left: 0;
}
50% {
left: 300px;
}
100% {
left: 0;
}
}
@keyframes transformMove {
0% {
transform: translateX(0);
}
50% {
transform: translateX(300px);
}
100% {
transform: translateX(0);
}
}
.container {
margin-top: 150px;
padding: 20px;
}
button {
margin-top: 40px;
padding: 12px 24px;
background-color: #10b981;
color: white;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="container">
<div class="box left-box">left</div>
<div class="box transform-box">transform</div>
<button onclick="deadLoop()">触发死循环阻塞主线程</button>
</div>
<script>
function deadLoop() {
while (true) {}
}
</script>
</body>
</html>

总结
完整流程如下图:

- HTML 解析:将 HTML 字符串解析为描述页面结构的 DOM 树和整合所有样式规则的 CSSOM 树。
- 样式计算:遍历 DOM 树,结合 CSSOM 树为每个节点计算最终样式,生成带有样式的 DOM 树。
- 布局:根据带样式的 DOM 树计算每个节点的几何信息,生成仅包含可见元素的 Layout 树。
- 分层:将 Layout 树划分为多个图层,实现局部更新优化。
- 绘制:为每个图层生成详细的绘制指令集。
- 分块:合成线程将每个图层切割为小块,优先处理视口内区域。
- 光栅化:GPU 进程将分块的绘制指令转换为位图,交给合成线程。
- 合成绘制:合成线程生成 quad 信息,GPU 结合 quad 将所有位图合成并输出到屏幕成像。
重排 Reflow
Reflow 即重排,也称回流 。它是指当 DOM 元素的几何属性 (如尺寸、位置、数量、隐藏 / 显示)发生变化时,浏览器需要重新计算元素的几何信息(位置、大小等)的过程。
重排发生在整个渲染流程的布局阶段,它的本质是重新计算 Layout 树,是布局的"推倒重来"。
触发重排的常见场景:
- 修改元素的宽高(
width
、height
)、外边距(margin
)、内边距(padding
) - 改变元素的定位方式(
position: absolute
改为relative
) - 增减 DOM 元素(如
appendChild
、removeChild
) - 浏览器窗口大小变化(
resize
事件) - 读取或修改某些会触发布局的属性(如
offsetWidth
、scrollTop
)- 浏览器为返回最新值,会立即触发重排
重排的代价很高 ,因为一个元素的几何变化可能会影响其他元素的位置(比如父元素尺寸变化会导致子元素重新排列)。浏览器会从该元素开始,向上遍历 DOM 树,重新计算所有受影响元素的布局,形成 "重排链"。
为了避免连续的多次操作 导致布局树反复计算,浏览器会合并 这些操作,当 JS 代码全部完成后再进行统一计算 。所以,改动属性造成的 Reflow 是异步完成的。
也同样因为如此,当 JS 获取布局属性 时,就可能造成无法获取到最新 的布局信息。所以浏览器的策略是,当要获取布局属性会立即 Reflow,以返回最新值。
javascript
// 连续修改样式会被浏览器合并为一次重排(异步)
box.style.width = "200px";
box.style.height = "150px";
// 读取布局属性会强制触发重排(同步)
console.log(box.offsetWidth); // 触发重排以获取最新值
💎 常见优化手段
基于上述重排机制,在日常开发中我们就可以有如下考虑重排的优化手段:
- 批量修改样式 :通过
class
一次性切换多个样式,而非逐条修改style
属性:
javascript
// 优化前:多次修改可能触发多次重排(实际被浏览器合并,但仍有风险)
box.style.width = "200px";
box.style.height = "150px";
// 优化后:一次class切换完成所有修改
box.classList.add("new-size"); // CSS中定义.new-size { width: 200px; height: 150px; }
- 避免强制同步布局:先修改样式,再集中读取布局属性:
javascript
// 优化前:读取→修改→读取,触发两次重排
const width = box.offsetWidth;
box.style.width = width + 10 + "px";
console.log(box.offsetWidth);
// 优化后:先修改,再读取,仅触发一次重排
box.style.width = box.offsetWidth + 10 + "px";
console.log(box.offsetWidth);
- 使用 "离线 DOM" :对复杂 DOM 修改,先将元素脱离文档流(如
display: none
),修改后再恢复:
javascript
const list = document.getElementById("list");
list.style.display = "none"; // 脱离文档流,后续修改不触发重排
// 执行大量DOM操作...
list.appendChild(newItem);
list.appendChild(anotherItem);
list.style.display = "block"; // 仅触发一次重排
重绘 Repaint
Repaint 即重绘 ,是指当 DOM 元素的非几何属性发生变化 (如颜色、背景、阴影)时,浏览器不需要重新计算元素的几何信息,只需重新绘制元素外观的过程。
重绘发生在整个渲染流程的绘制阶段 ,它的本质是为可见样式改变的元素重新生成渲染指令,是外观的"局部刷新"。
由于元素的布局信息也属于可见样式,所以 Reflow 一定会引起 Repaint 。
反之,Repaint 不一定会触发 Reflow
写在最后
One day you'll leave this world behind. So live a life you will remember! --- Avicii
我是暮星,一枚有志于在前端领域证道的攻城狮。
优质前端内容持续输出中......,欢迎点赞 + 关注 + 收藏。