POST://browser-render-pipeline
带点橘子味的馒头的头像
带点橘子味的馒头

FRONTEND / DESKTOP DEV

返回文章列表

浏览器是怎么把代码变成像素的:渲染流水线拆解

1 分钟· 883 2 次阅读

"为什么我改了个颜色,整个页面都卡了一下?"——这种问题的答案,藏在浏览器的渲染流水线里。理解它,你写的每一行 CSS/JS 才知道自己到底贵不贵。

一条流水线,五个阶段

浏览器把一份文档变成屏幕上的像素,大致要走完这几步:

HTML ──▶ DOM 树
CSS  ──▶ CSSOM 树
              │
              ▼
         Render Tree(渲染树,只含可见节点)
              │
              ▼
          Layout(布局 / 重排:算每个盒子的位置与尺寸)
              │
              ▼
          Paint(绘制:把盒子画成像素,分层)
              │
              ▼
        Composite(合成:把各层交给 GPU 拼到屏幕上)

关键认知:前面任何一个阶段变了,它后面的阶段都得重跑。这就是为什么"动一下就卡"。

重排(Reflow)最贵

Layout 阶段即重排。以下操作会触发它:

  • width / height / margin / top 等几何属性;
  • 增删 DOM 节点;
  • 读取会强制同步布局的属性(offsetTopgetBoundingClientRect() 等)。

重排往往不是孤立的——一个元素尺寸变了,可能影响父级、兄弟、甚至整列,浏览器会做"脏区域"传播,代价从局部扩散到全局。

重绘(Repaint)次之

Paint 阶段把盒子描边、填色。改 colorbackgroundbox-shadowvisibility 会触发重绘,但不触发重排。重绘比重排便宜,因为它不需要重新算位置;但大面积重绘(比如全屏渐变背景动画)依然能拖垮帧率。

合成(Composite)最便宜

只改 transformopacity 时,浏览器通常只做合成:把已经画好的图层交给 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 占了主线程。

小结

渲染流水线的本质是"一环变、环环重算"。写样式和动画时记住三句话:

  1. 动画优先 transform / opacity,它们只走最便宜的合成;
  2. 读写分离,别在循环里交替读布局属性与写样式;
  3. 先测量再优化,Performance 面板比直觉靠谱。

把这三点到位,大部分"改个样式就卡"的问题都会消失。

相关文章