# 浏览器渲染管线详解
用生活化的比喻,让你彻底理解浏览器是如何把 HTML/CSS/JS 变成屏幕上的像素的
前置知识:2_1_v8Learn(JS执行原理)+ 2_2_h5-rendering-mastery(GPU渲染管线)
阅读指南(初学者必看)
为什么你需要学习浏览器渲染管线?
你已经学了 V8 引擎(JS 怎么被编译和执行)和 GPU 渲染管线(GPU 怎么把顶点数据变成像素)。但这两个知识之间有一条裂缝:
- V8 项目讲了 JS 执行,但没讲浏览器拿到 JS 执行结果后怎么渲染
- GPU 渲染项目讲了 GPU 的工作原理,但没讲浏览器怎么把 HTML/CSS 变成 GPU 能理解的数据
- 你修改 DOM 后页面变卡了,但不知道是重排还是重绘导致的
学完本章,你能回答:
- 浏览器从收到 HTML 到渲染出像素,经过了哪些步骤?
- 为什么修改 DOM 会卡?哪些操作导致重排,哪些只导致重绘?
- 合成层是什么?为什么 100 个合成层会占 800MB 显存?
本文结构
第一部分:从 URL 到像素的完整流程(建立全景认知)
第二部分:DOM 构建(HTML 怎么变成 DOM 树)
第三部分:CSSOM 与样式计算(CSS 怎么影响渲染)
第四部分:Render Tree 与布局(怎么计算元素的位置和大小)
第五部分:绘制与合成(怎么把元素变成像素)
第六部分:合成层深度解析(H5 游戏性能优化的核心概念)
一、从 URL 到像素:完整流程
生活类比:浏览器渲染就像一条汽车生产线。
原材料(URL) → 采购(网络请求) → 拆箱(HTML解析) →
组装骨架(DOM) → 喷漆方案(CSSOM) → 设计图纸(Render Tree) →
计算尺寸(Layout) → 喷漆(Paint) → 贴膜合成(Composite) → 成品(像素)
| 阶段 | 做了什么 | 耗时在哪 | 游戏中的影响 |
|---|---|---|---|
| 导航 | DNS → TCP → TLS → HTTP | 网络延迟 | 首屏加载速度 |
| 解析 | HTML → DOM,CSS → CSSOM | 大文档解析 | 初始化时间 |
| 渲染树 | DOM + CSSOM → Render Tree | 不可见节点过滤 | — |
| 布局 | 计算 DOM 元素位置和大小 | 复杂布局 | DOM 操作导致重排 |
| 绘制 | 生成绘制指令列表 | 大面积/复杂样式 | 重绘开销 |
| 合成 | GPU 合成各层输出像素 | 层数/纹理大小 | 显存占用 |
浏览器渲染管线的 8 大阶段
HTML/CSS/JS
|
v
1. HTML解析 ----> DOM树
|
2. CSS解析 -----> CSSOM树
|
3. 合并 --------> Render树(渲染树)
|
4. 布局 --------> 几何信息(位置和大小)
|
5. 分层 --------> 层树(Layer Tree)
|
6. 绘制 --------> 绘制指令列表
|
7. 光栅化 ------> 位图(Bitmap)
|
8. 合成 --------> 屏幕像素
|
+-----------> 显示到屏幕
关键认知:不是每次页面变化都要走完整的 8 个阶段。浏览器非常聪明,会根据变化的类型跳过不必要的阶段。
| 变化类型 | 需要重新执行的阶段 | 跳过的阶段 |
|---|---|---|
| 改变元素颜色 | 6绘制 + 7光栅化 + 8合成 | 1,2,3,4,5 |
| 改变元素尺寸 | 4布局 + 5分层 + 6,7,8 | 1,2,3 |
| 使用 transform 移动 | 8合成 | 1,2,3,4,5,6,7 |
| display:none→block | 3,4,5,6,7,8 | 1,2 |
为什么 transform 只需要重新合成? 因为 transform 只影响元素的位置变换,不改变元素的内容、大小或样式。浏览器可以把元素预先绘制在一个独立的层(Layer)上,transform 只是改变这个层的位置或变换矩阵。 类比:你有一张已经画好的画(绘制完成),现在你只是把画框移动到房间的另一面墙上(transform)。你不需要重新画画,只需要移动画框。
二、DOM 构建
生活类比:DOM 构建就像搭积木。HTML 是说明书,浏览器按顺序一块一块搭。
HTML 字符串
↓ tokenizer(切词)
Token 流(<div>, <p>, "文本", </p>, </div>)
↓ tree construction(建树)
DOM 树
关键细节1:预扫描器
浏览器不会等 JS 下载完再继续解析 HTML。预扫描器会提前扫描后面的 <link> 和 <script>,并行下载资源。
HTML 解析器遇到 <script src="app.js">:
├─ 主解析器:暂停,等 JS 下载并执行
└─ 预扫描器:继续扫描后面的 HTML,发现 <link> 和 <script> 就提前下载
对游戏开发的影响:
- 游戏加载页面要把关键 JS 放在
<head>里加defer - 非关键的资源(广告SDK、统计SDK)用
async异步加载
关键细节2:async vs defer
<!-- 方式1:阻塞解析,下载完立即执行 -->
<script src="critical.js"></script>
<!-- 方式2:不阻塞解析,下载完立即执行(执行顺序不保证) -->
<script async src="analytics.js"></script>
<!-- 方式3:不阻塞解析,等 HTML 解析完再执行(按顺序) -->
<script defer src="game-engine.js"></script>
<!-- 方式4:默认 defer 行为 -->
<script type="module" src="game.js"></script>
| 属性 | 下载时机 | 执行时机 | 执行顺序 | 适用场景 |
|---|---|---|---|---|
| 无 | 遇到就下载 | 下载完立即执行 | 按出现顺序 | 关键脚本 |
async |
遇到就下载(并行) | 下载完立即执行 | 不保证顺序 | 统计/广告SDK |
defer |
遇到就下载(并行) | HTML解析完后执行 | 按出现顺序 | 游戏引擎/框架 |
type="module" |
遇到就下载(并行) | HTML解析完后执行 | 按依赖关系 | 现代JS模块 |
关键细节3:DOM 操作很慢
每次修改 DOM 都可能触发重排或重绘。这不是 DOM API 本身慢,而是浏览器的渲染管线很重。
// ❌ 每次修改都触发重排
for (let i = 0; i < 100; i++) {
element.style.left = i + 'px'; // 100次重排!
}
// ✅ 批量修改,只触发1次重排
element.style.cssText = 'left: 100px;'; // 只1次重排
// ✅ 使用 DocumentFragment 批量插入
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}
list.appendChild(fragment); // 只1次重排
三、CSSOM 与样式计算
生活类比:CSSOM 就像装修方案表。每个 DOM 节点对应一份"怎么装修"的说明。
CSS 选择器匹配:从右到左
/* 浏览器是这样匹配的:先找所有 .item,再看祖先有没有 .list */
.list .item { color: red; }
/* 所以这个选择器很慢:先找所有 div,再看祖先有没有 .list */
/* 页面里有1000个div,每个都要检查祖先 → 1000次检查! */
.list div { color: red; }
性能建议:
- 避免使用通配选择器
* - 避免标签选择器
.list div,改用类选择器.list-item - 选择器层级不要太深(3层以内)
样式级联顺序
!important > 内联 style > #id > .class / :pseudo > tag > 继承
↑ 优先级最高 ↑ 最常用 ↑ 最低
CSS 阻塞渲染
为什么 CSS 会阻塞渲染?因为不知道样式就没法计算布局。
HTML 解析中 ──遇到 <link>──▶ 下载 CSS ──▶ 解析 CSSOM ──▶ 继续解析 HTML
│
└── 这段时间页面白屏!
优化建议:
- 关键 CSS 内联到
<head>中(首屏渲染需要的样式) - 非关键 CSS 用
media属性延迟加载
<link rel="stylesheet" href="critical.css"> <!-- 阻塞渲染 -->
<link rel="stylesheet" href="print.css" media="print"> <!-- 只在打印时加载 -->
四、Render Tree 与布局(Layout/Reflow)
生活类比:Render Tree 是"真正要画的东西"。display:none 的元素就像被划掉的名字,不在名单上。
Render Tree 的构成
DOM 树: Render Tree:
├── html ├── RenderView (viewport)
│ ├── head │ ├── RenderBody
│ │ └── ... (不渲染) │ │ ├── RenderDiv (header)
│ └── body │ │ └── RenderDiv (main)
│ ├── div#header │ │ ├── RenderText ("Hello")
│ │ └── "Hello" │ │ └── RenderDiv (sidebar)
│ ├── div#main │ │ ├── RenderText ("Content")
│ │ └── "Content" │ │ └── ...
│ └── div.hidden │ └── ...
│ └── "Hidden" ← 不在 │
Render Tree 中 │
Layout(布局/重排):计算元素的位置和大小
触发重排的操作:
| 操作类型 | 具体操作 | 影响范围 |
|---|---|---|
| 添加/删除元素 | appendChild、removeChild |
全局或局部 |
| 修改尺寸 | width、height、margin、padding |
全局或局部 |
| 读取布局属性 | offsetWidth、getBoundingClientRect() |
强制同步布局! |
| 修改字体 | fontSize、fontFamily |
全局 |
| 修改内容 | textContent、innerHTML |
局部 |
| 窗口变化 | resize 事件 |
全局 |
强制同步布局(Layout Thrashing):最危险的重排模式!
// ❌ 强制同步布局:读→写→读→写,每次读都强制浏览器立即计算布局
for (let i = 0; i < 100; i++) {
const width = element.offsetWidth; // 读:强制布局计算
element.style.width = width * 2 + 'px'; // 写:使布局失效
// 下一次循环的 offsetWidth 又会强制布局 → 100次重排!
}
// ✅ 批量读取,批量写入
const width = element.offsetWidth; // 读1次
element.style.width = width * 2 + 'px'; // 写1次
// 只有1次重排
布局边界(Layout Boundary)
修改某个元素的布局不会影响祖先时,这个元素就是布局边界。
创建布局边界的方法:
position: absolute或position: fixedoverflow: hidden(不是visible)display: flex/display: grid
// 游戏中的最佳实践:
// 把会频繁变化的元素放在 absolute 容器中
// 这样变化不会影响其他元素的布局
const popup = document.createElement('div');
popup.style.position = 'absolute'; // 创建布局边界
popup.style.left = '100px';
popup.style.top = '100px';
五、绘制与合成
生活类比:绘制就像画家一笔一笔画,合成就像把多张透明胶片叠在一起。
绘制(Paint)
绘制是生成绘制指令列表的过程。这些指令包括:
- 画矩形、画圆、画文字
- 填充颜色、画边框
- 画图片、画阴影
触发重绘的操作(不会触发重排):
- 修改
color、background-color - 修改
box-shadow - 修改
visibility: hidden(注意不是display:none)
重排 vs 重绘的代价:
重排(Layout)= 重新计算位置 + 重新绘制
重绘(Paint)= 只重新绘制
所以:重排一定导致重绘,重绘不一定导致重排
合成(Composite)
合成是把各层的绘制结果交给 GPU,由 GPU 把多层叠在一起输出最终像素。
合成的优势:
- 只需要 GPU 参与,不需要 CPU 重新计算布局和绘制
transform和opacity的变化只触发合成,不触发重排和重绘- 这就是为什么 CSS 动画推荐用
transform而不是left/top
/* ❌ 触发重排+重绘 */
.animate-bad {
transition: left 0.3s, top 0.3s;
}
/* ✅ 只触发合成,性能最佳 */
.animate-good {
transition: transform 0.3s, opacity 0.3s;
}
六、合成层深度解析
这是 H5 游戏性能优化的核心概念!
什么元素会成为合成层?
| 触发条件 | 说明 | 游戏中的场景 |
|---|---|---|
will-change: transform |
显式声明将要变化 | 游戏角色动画 |
will-change: opacity |
显式声明将要变化 | 淡入淡出效果 |
| 3D transform | translate3d、translateZ(0) |
最常见的hack:强制GPU加速 |
opacity 动画 |
CSS opacity 动画 | UI 淡入淡出 |
<video> |
视频元素 | 游戏CG |
<canvas> |
Canvas 元素 | WebGL游戏本身 |
<iframe> |
内嵌页面 | 广告、支付 |
position: fixed |
固定定位 | 游戏HUD |
| 隐式提升 | 与合成层重叠 | ⚠️ 可能导致层爆炸! |
合成层的内存成本
每层纹理大小 = width × height × 4 bytes(RGBA8)
加上 Mipmap ≈ 额外 33%
例子:一个 1920×1080 的合成层
= 1920 × 1080 × 4 = 8,294,400 bytes ≈ 8MB
100 个合成层 ≈ 800MB 显存!
对 H5 游戏的影响:
- 一个 Canvas 游戏本身就是1个合成层
- 如果你给每个 UI 元素都加了
will-change: transform…… - 50 个 UI 元素 = 50 个合成层 = 400MB 显存!
- 低端手机可能只有 1~2GB 总内存,400MB 纯给合成层就直接爆了
层爆炸
层爆炸:隐式提升导致合成层数量失控。
正常情况:
div.container(普通层)
└── div.animated(合成层,will-change: transform)
如果 container 和 animated 重叠,浏览器可能需要把 container 也提升为合成层
→ 2个合成层
如果有 20 个这样的 animated 元素互相重叠呢?
→ 20+ 个合成层!这就是层爆炸
如何检测层爆炸:
- 打开 Chrome DevTools → More tools → Layers
- 查看层数量和每层的大小
- 或者:DevTools → Rendering → Layer borders(显示层的边框)
如何修复层爆炸:
- 移除不必要的
will-change - 给需要动画的元素加
z-index并position: relative/absolute(隔离层叠上下文) - 减少合成层之间的重叠区域
- 使用
transform: translateZ(0)代替will-change(更精确地控制)
H5 游戏中的合成层策略
✅ 推荐做法:
1. Canvas/WebGL 游戏本身就是1个合成层,不需要额外优化
2. UI 元素不要随意加 will-change
3. 只有频繁动画的元素才需要提升为合成层
4. 动画结束后移除 will-change(节省显存)
❌ 常见错误:
1. 给所有元素加 will-change "以防万一" → 层爆炸
2. 用 translateZ(0) 给所有元素"GPU加速" → 每个都是合成层
3. 不清理动画结束后的合成层 → 显存不释放
自问自答
Q:重排和重绘哪个更严重? A:重排更严重。重排 = 重新计算布局 + 重新绘制,重绘 = 只重新绘制。重排一定导致重绘,但重绘不一定导致重排。
Q:为什么 CSS 动画推荐用 transform 而不是 left/top? A:transform 的变化只触发合成(Composite),不触发重排(Layout)和重绘(Paint)。而 left/top 的变化会触发重排+重绘。合成只在 GPU 上执行,不占用 CPU。
Q:display:none 和 visibility:hidden 有什么区别?
A:display:none 让元素不在 Render Tree 中(不占空间,不渲染)。visibility:hidden 让元素在 Render Tree 中(占空间,但不可见)。前者不参与任何渲染步骤,后者仍然参与布局和绘制,只是不显示。
Q:游戏里应该用 DOM 还是 Canvas? A:Canvas/WebGL 游戏只有1个合成层,内存占用可控。DOM 游戏如果 UI 元素多,容易层爆炸。但 DOM 对文本渲染和交互更友好。最佳实践:游戏画面用 Canvas,UI 用 DOM 但严格控制合成层。
实践任务
- 任务1:用 Chrome DevTools Layers 面板分析一个网页的合成层数量和显存占用
- 任务2:制造"层爆炸"——创建多个重叠元素 +
will-change: transform,观察显存变化 - 任务3:修复层爆炸——移除不必要的
will-change,用z-index隔离层叠上下文 - 任务4:对比
transform动画和left/top动画的性能差异(用 Performance 面板录制) - 任务5:制造强制同步布局,用 Performance 面板中的红色警告标记定位问题代码
与其他章节的关联
| 本章内容 | 关联章节 | 关联点 |
|---|---|---|
| 合成层 | 第02章 | 合成层占用的显存是游戏内存的大头 |
| 重排/重绘 | 第03章 | 长任务阻塞渲染导致掉帧 |
| DOM 构建 | 2_1_v8Learn | V8 解析执行 JS 可能阻塞 DOM 构建 |
| GPU 合成 | 2_2_h5-rendering-mastery | GPU 渲染管线的最后一步 |
下一章:02-合成层与GPU加速