深入理解浏览器渲染流程
0. 事件循环复习
我们之前总结过:事件循环是主线程的工作方式,每执行完一个宏任务,就清空所有微任务,然后可能渲染页面,再取下一个宏任务。
重点来了:渲染到底是怎么发生的? 这就是本篇文章要讲的内容。
1. 为什么需要了解渲染流程?
你每天都在写 HTML、CSS、JS,但浏览器到底是怎么把它们变成屏幕上像素的?
搞懂渲染流程,你就能明白:
- 为什么改
left/top会卡,改transform却很丝滑 - 为什么有些 CSS 属性改了开销大,有些开销小
- 面试官问“重排重绘”时该怎么答
这是前端性能优化的基础,也是面试必考题。
2. 渲染流程五步走
浏览器拿到 HTML 和 CSS 后,会按顺序做这 5 件事:
| 步骤 | 名称 | 做了什么 |
|---|---|---|
| 1 | 构建 DOM 树 | 把 HTML 标签转成树形结构 |
| 2 | 构建 CSSOM 树 | 把 CSS 规则转成树形结构 |
| 3 | 构建渲染树 | 合并 DOM 和 CSSOM,过滤掉不可见元素 |
| 4 | 布局(Layout) | 计算每个元素的位置和大小 |
| 5 | 绘制(Paint) | 把像素画到屏幕上 |
第 4 步也叫 重排(Reflow),第 5 步也叫 重绘(Repaint)。
如下图:
2.1 构建 DOM 树
浏览器从上到下解析 HTML,把标签转成树形结构的 DOM 对象。
例如:
<html>
<body>
<div>hello</div>
</body>
</html>
会变成类似这样的结构(伪代码):
document
└ html
└ body
└ div → text "hello"
注意:<script> 标签会阻塞解析,因为 JS 可能修改 DOM。可以加 defer 或 async 避免阻塞。
2.2 构建 CSSOM 树
浏览器解析 CSS 文件或 <style> 标签内的样式,构建成 CSSOM 树(CSS 对象模型)。
CSSOM 记录了选择器与样式规则的对应关系,以及继承关系(比如 body 的 font-size 会传给子元素)。
CSS 不会阻塞 DOM 树的构建,但会阻塞渲染(因为需要完整的样式才能绘制)。
2.3 构建渲染树(重点)
渲染树 = DOM 树 + CSSOM 树,但会过滤掉不需要显示的东西。
具体操作:
- 只保留能看见的元素
display: none的元素不进入渲染树(连占位都没有)<head>标签里的元素不进入渲染树visibility: hidden的元素会进入渲染树(它占位置,只是看不见)opacity: 0的元素也会进入渲染树(透明也是可见的一种)
- 给每个节点附上计算好的样式
从 CSSOM 里找到匹配的规则,经过层叠、继承、优先级计算,得到每个节点的最终样式。
示例:
<div style="display: none;">看不见我</div>
<div>看得见我</div>
渲染树里只有第二个 div,第一个直接被丢掉了。
为什么需要渲染树?
因为 DOM 树里有很多不参与页面绘制的节点(head、script、display: none 的元素),直接拿着 DOM 树去布局会浪费性能。渲染树就是“最终要画到屏幕上的东西”的清单。
2.4 布局(Layout / 重排)
遍历渲染树,计算每个元素在屏幕上的精确位置和尺寸(宽、高、x、y)。
比如一个 div 宽度是父容器的 50%,就要算出实际像素值。
触发布局的情况:
- 首次渲染
- 窗口
resize - 修改元素的几何属性**(宽/高/边距/位置)**
- 添加/删除 DOM
- 读取某些属性(
offsetHeight、getComputedStyle等)
布局是开销最大的步骤。
2.5 绘制(Paint / 重绘)
把每个元素画成像素:背景、边框、文字、阴影、图片等。
浏览器会把页面分成多个图层,分别绘制,最后合成。
触发绘制的情况:
- 改变背景色、文字颜色、边框颜色等(不影响位置)
3. 重排 vs 重绘(核心重点)
这两个概念必须分清。
| 对比项 | 重排(Reflow) | 重绘(Repaint) |
|---|---|---|
| 什么时候发生 | 改宽高、边距、位置、增删 DOM、改字体等 | 改颜色、背景、阴影、可见性等 |
| 开销 | 很大(重新计算位置) | 中等(只重新涂色) |
| 会触发另一个吗 | 会,重排一定导致重绘 | 不会,重绘不一定导致重排 |
| 优化建议 | 尽量避免,或用 transform 替代 | 可接受,但不要频繁 |
3.1 代码示例
// 坏:触发重排
box.style.width = '200px'
box.style.height = '200px'
box.style.margin = '10px'
// 好:合并修改,只触发一次重排
box.style.cssText = 'width:200px; height:200px; margin:10px;'
// 更好:用 transform 做动画,完全不触发重排/重绘
box.style.transform = 'translateX(100px)'
4. 哪些操作会触发重排?
- 改
width/height/margin/padding/border - 改
font-size(文字大小影响盒子大小) - 改
display(比如none→block) - 添加或删除 DOM 元素
- 改变窗口大小
- 读取某些属性:
offsetHeight、offsetTop、scrollTop、getComputedStyle等(浏览器被迫立即重排)
最后一条只是读一下,浏览器也得乖乖重排才能给你准确值。所以不要在循环里读这些属性。
5. 如何减少重排?
| 优化手段 | 说明 |
|---|---|
| 合并样式修改 | 用 cssText 或切换 class,不要一条一条改 |
| 让元素脱离文档流 | position: absolute 或 fixed,它的重排不影响别人 |
| 批量插入 DOM | 用 documentFragment 先组装好,再一次性插入 |
动画用 transform | transform 走合成线程,不触发重排/重绘 |
| 避免读触发布局的属性 | 不要频繁读 offsetHeight 等,如果必须读,先读好存起来 |
5.1 批量插入 DOM 示例
// 坏:每次插入都触发重排
for (let i = 0; i < 100; i++) {
document.body.appendChild(div)
}
// 好:用 fragment 一次性插入
const fragment = document.createDocumentFragment()
for (let i = 0; i < 100; i++) {
fragment.appendChild(div)
}
document.body.appendChild(fragment) // 只触发一次重排
6. transform 为什么快?
transform 不走布局和绘制,它直接进入合成阶段,由 GPU 处理。
简单理解:
left/top:改位置 → 触发重排 → 重绘 → 合成(主线程干,慢)transform:跳过前两步 → 直接合成(合成线程干,快)
所以做动画时,能用 transform 就别用 left/top。
/* 慢 */
.box {
transition: left 0.3s;
left: 0;
}
.box.active {
left: 100px;
}
/* 快 */
.box {
transition: transform 0.3s;
transform: translateX(0);
}
.box.active {
transform: translateX(100px);
}
7. 常见面试题
7.1 重排和重绘的区别?哪个更耗性能?
重排是重新计算位置和大小,开销大;重绘是重新涂色,开销中等。重排一定触发重绘,反之不一定。
7.2 哪些属性会触发重排?
width、height、margin、padding、border、font-size、display、position 等。还有添加/删除 DOM、改窗口大小。
7.3 如何避免重排?
- 合并样式修改
- 使用
transform做动画 - 批量操作 DOM
- 让元素脱离文档流
7.4 transform 和 left/top 有什么区别?
left/top 触发布局(重排),慢;transform 只触发合成,由 GPU 处理,快。
7.5 为什么有时候读 offsetHeight 会让页面变慢?
因为浏览器需要立即计算最新的布局才能返回准确值,这会强制重排。如果在循环里读,会反复触发重排,性能极差。
8. 总结一句话
浏览器渲染分五步:DOM 树 → CSSOM 树 → 渲染树 → 布局(重排)→ 绘制(重绘)。
重排慢,重绘快,动画用 transform最流畅。
优化核心:减少重排,合并操作,能用合成就合成。