浏览器是怎么把代码变成像素的:渲染流水线拆解
"为什么我改了个颜色,整个页面都卡了一下?"——这种问题的答案,藏在浏览器的渲染流水线里。理解它,你写的每一行 CSS/JS 才知道自己到底贵不贵。
一条流水线,五个阶段
浏览器把一份文档变成屏幕上的像素,大致要走完这几步:
HTML ──▶ DOM 树
CSS ──▶ CSSOM 树
│
▼
Render Tree(渲染树,只含可见节点)
│
▼
Layout(布局 / 重排:算每个盒子的位置与尺寸)
│
▼
Paint(绘制:把盒子画成像素,分层)
│
▼
Composite(合成:把各层交给 GPU 拼到屏幕上)
关键认知:前面任何一个阶段变了,它后面的阶段都得重跑。这就是为什么"动一下就卡"。
重排(Reflow)最贵
Layout 阶段即重排。以下操作会触发它:
- 改
width / height / margin / top等几何属性; - 增删 DOM 节点;
- 读取会强制同步布局的属性(
offsetTop、getBoundingClientRect()等)。
重排往往不是孤立的——一个元素尺寸变了,可能影响父级、兄弟、甚至整列,浏览器会做"脏区域"传播,代价从局部扩散到全局。
重绘(Repaint)次之
Paint 阶段把盒子描边、填色。改 color、background、box-shadow、visibility 会触发重绘,但不触发重排。重绘比重排便宜,因为它不需要重新算位置;但大面积重绘(比如全屏渐变背景动画)依然能拖垮帧率。
合成(Composite)最便宜
只改 transform 和 opacity 时,浏览器通常只做合成:把已经画好的图层交给 GPU 平移/缩放/淡入,完全不碰布局和绘制。这是动画性能的天花板——想做丝滑动画,优先用 transform 而不是 left/top。
| 操作 | 触发阶段 | 相对代价 |
|---|---|---|
改 transform / opacity |
仅合成 | 低 ✅ |
改 color / background |
重绘 | 中 |
改 width / top |
重排 → 重绘 → 合成 | 高 ⚠️ |
读 offsetTop 后写样式 |
强制同步重排 | 高 ⚠️ |
两个实战技巧
1. 把"读"和"写"分开
下面这种循环是性能杀手——每轮都强制浏览器同步重排:
// ❌ 读一次写一次,浏览器被迫在每轮之间重排
for (const el of boxes) {
const h = el.offsetHeight; // 读 → 强制布局
el.style.height = h * 2 + "px"; // 写 → 标记脏
}
改成先读后写,浏览器只需重排一次:
// ✅ 批量读,再批量写
const heights = boxes.map((el) => el.offsetHeight);
boxes.forEach((el, i) => {
el.style.height = heights[i] * 2 + "px";
});
2. 用 will-change 提升图层,但别滥用
.card {
will-change: transform; /* 提示浏览器提前为该元素建独立图层 */
}
图层能避免动画时反复重绘,但每个图层都占内存。给几十上百个元素都加 will-change,反而会因为 GPU 显存和合成开销拖慢整体。正确用法:只在真正要动画的元素、且动画期间加,结束后移除。
用 devtools 验证,别猜
Chrome DevTools 的 Performance 面板录制一段交互,能直接看到每一帧的耗时分布:哪一段是 Scripting、Rendering 还是 Painting。遇到卡顿,先录一段,看火焰图里谁最长,再对症下药——是重排炸了,还是某段 JS 占了主线程。
小结
渲染流水线的本质是"一环变、环环重算"。写样式和动画时记住三句话:
- 动画优先
transform/opacity,它们只走最便宜的合成; - 读写分离,别在循环里交替读布局属性与写样式;
- 先测量再优化,Performance 面板比直觉靠谱。
把这三点到位,大部分"改个样式就卡"的问题都会消失。