每次你打开一个网页,浏览器都在飞速完成一条"流水线"——把你写的 HTML/CSS/JS 变成屏幕上一个个可见的像素。理解这条流水线,是写出高性能前端的基础。
第一步:解析(Parse)
浏览器收到服务器返回的 HTML 字节流后,做两件事:
构建 DOM 树:解析 HTML 标签,形成一棵节点树(Document Object Model)。遇到 <script> 标签,默认会暂停解析,等 JS 执行完再继续——这就是为什么 <script> 要放在 </body> 前,或加 defer / async 属性。
构建 CSSOM 树:解析所有 CSS(外部文件、<style> 标签、内联样式),构建 CSS Object Model。CSS 是"阻塞渲染"的——CSSOM 没建完,浏览器不会开始渲染。> 开发者须知:display: none 的元素不会出现在渲染树里;visibility: hidden 会占位但不显示;opacity: 0 完全可见但透明——三者对渲染流水线的影响完全不同。
第二步:样式计算(Style)
浏览器将所有 CSS 规则"匹配"到 DOM 节点上,计算出每个节点最终生效的样式(Computed Style)。这里有几个关键点:
- 级联(Cascade):多条规则冲突时,按优先级(
!important> 内联 > ID > 类 > 标签)决定胜者 - 继承(Inheritance):
font-size、color等属性会从父节点向子节点传递 - 相对值转绝对值:
em、rem、%、vh全部在这一步转换成像素值
第三步:布局(Layout / Reflow)
拿到渲染树和每个节点的样式后,浏览器要算出每个元素的几何信息:在页面上的位置 (x, y) 和尺寸 (width, height)。这个过程也叫 Reflow(回流)。典型的强制同步布局(性能杀手):
// ❌ 错误示范:在循环里交替写/读布局属性
for (let i = 0; i < items.length; i++) {
items[i].style.width = container.offsetWidth + 'px' // 读 → 触发回流 → 写 → 循环
}
// ✅ 正确做法:先读后写,批量操作
const w = container.offsetWidth // 只读一次
for (let i = 0; i < items.length; i++) {
items[i].style.width = w + 'px'
}
第四步:分层(Layer)
布局完成后,浏览器会把页面划分成多个图层(Layer)。并非所有元素都是独立图层,只有满足特定条件的元素才会被"提升":
- 有
transform/opacity动画的元素 position: fixed的元素- 有
will-change属性的元素 <video>、<canvas>、<iframe>等
图层的意义在于:某个图层发生变化时,只需重新处理该图层,其他图层不受影响。
第五步:绘制(Paint / Repaint)
浏览器遍历每个图层,将元素转换成绘制指令列表(Draw calls),例如"在坐标(x,y)画一个圆角矩形,颜色 #fff,边框 1px #eee"。这个过程叫 Repaint(重绘)。
重绘比回流便宜,但修改 color、background、box-shadow 等视觉属性都会触发它。---
第六步:合成(Composite)
各个图层的绘制指令发送给 GPU,在独立的合成线程(Compositor Thread)上完成最终的像素合成输出到屏幕。
这一步完全绕开了主线程!这意味着:用 transform 和 opacity 做动画,即使主线程卡住,动画依然流畅。 这是 CSS 动画优于 JS 动画的核心原因之一。
实战建议:写出"渲染友好"的代码---
小结
| 阶段 | 做了什么 | 触发条件 |
|---|---|---|
| 解析 | HTML → DOM,CSS → CSSOM | 首次加载,JS 修改 DOM |
| 样式计算 | 匹配规则,算 Computed Style | 样式表/类名变化 |
| 布局 | 算坐标和尺寸 | 几何属性改变 |
| 分层 | 划分合成图层 | 特殊 CSS 属性 |
| 绘制 | 生成绘制指令 | 视觉属性改变 |
| 合成 | GPU 合并图层输出 | 每一帧都发生 |
核心结论:优化渲染性能的本质,就是尽量让变化只走流水线的后半段(合成),避免触发回流——因为回流会让整条流水线从布局重头跑一遍。