重排、重绘、合成:浏览器渲染的“三兄弟”,你惹不起也躲不过

0 阅读4分钟

你给一个元素悄悄改了宽度,结果整个页面都抖了一下?你加了个动画,电脑风扇开始狂转?今天我们来认识浏览器渲染里的“三兄弟”——重排、重绘、合成。弄懂它们,你就能写出流畅60帧的页面,告别卡顿。

前言

想象一下,你家客厅要重新装修。你只是换了个抱枕(重绘),很轻松。但如果你要把墙拆了(重排),那得搬家具、砸墙、重新粉刷,累得半死。如果只是把电视画面换个图层(合成),连工人都不要,遥控器一按就行。

浏览器的渲染也是这个道理。理解这三种操作的成本,就能写出性能飞起的页面。

一、先复习:渲染流水线

之前我们讲过,浏览器把HTML/CSS变成屏幕上的像素,要经过:DOM树 + CSSOM树 → 渲染树 → 布局(计算位置大小)→ 绘制(填充像素)→ 合成(合并图层)。

其中:

  • 重排(Reflow):重新计算布局(位置、大小)。成本最高。
  • 重绘(Repaint):重新绘制像素(颜色、背景、阴影等)。成本中等。
  • 合成(Composite):重新合并图层。成本极低(走GPU)。

二、重排:动到筋骨,全员遭殃

什么操作会触发重排?

  • 改变元素的几何属性widthheightmarginpaddingbordertopleft……
  • 改变DOM结构:增删元素、改变内容(文字变了导致高度变化)。
  • 读取某些布局属性offsetTopscrollTopclientWidthgetComputedStyle()。因为浏览器需要返回最新值,不得不强制重排。
  • 改变窗口大小(resize事件)。
  • 激活伪类(如:hover导致样式变化影响布局)。

重排的代价:浏览器要重新计算整个或部分渲染树,然后重新布局、绘制、合成。就像你拆了一面墙,整个房子都得重新量尺寸。

三、重绘:只换皮肤,不动骨架

什么操作会触发重绘但不触发重排?

  • 改变颜色colorbackground-colorborder-colorbox-shadow等。
  • 改变可见性visibility(但display: none会触发重排)。
  • 改变背景图outline等。

重绘的代价:不需要重新布局,但还是要重新绘制像素,比重排轻,但也不是免费。

四、合成:GPU加速的“超车道”

合成是成本最低的环节,因为它不涉及布局和绘制,只把已有的图层合并。能触发合成的属性有:

  • transform(平移、旋转、缩放)
  • opacity
  • filter

当你用transform: translateZ(0)will-change: transform时,浏览器会把这个元素提升到单独的合成层,后续动画只由GPU处理,完全不触发重排和重绘。这就是为什么动画推荐用transform而不是left

/* 差:触发重排 */
.box {
  transition: left 0.3s;
  left: 0;
}
.box:hover {
  left: 100px;
}

/* 好:只触发合成 */
.box {
  transition: transform 0.3s;
  transform: translateX(0);
}
.box:hover {
  transform: translateX(100px);
}

五、如何减少重排和重绘?

1. 批量修改样式

不要挨个改属性,用class一次改完:

// 差
element.style.width = '100px';
element.style.height = '100px';
element.style.margin = '10px';

// 好
element.classList.add('new-size');

2. 让元素脱离文档流再操作

比如要插入多个li,可以先隐藏(display: none),改完再显示,只触发两次重排。

const ul = document.getElementById('list');
ul.style.display = 'none';
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = i;
  ul.appendChild(li);
}
ul.style.display = 'block';

3. 使用文档片段(DocumentFragment)

const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = i;
  fragment.appendChild(li);
}
ul.appendChild(fragment); // 只触发一次重排

4. 读写分离

不要交替读取和修改布局属性,否则会触发多次重排。

// 差
for (let i = 0; i < boxes.length; i++) {
  boxes[i].style.width = boxes[i].offsetWidth + 'px'; // 读后立即写
}

// 好:先读后写
const widths = [];
for (let i = 0; i < boxes.length; i++) {
  widths.push(boxes[i].offsetWidth);
}
for (let i = 0; i < boxes.length; i++) {
  boxes[i].style.width = widths[i] + 'px';
}

5. 使用transformopacity做动画

永远不要用lefttopwidthmargin做动画,改用transform

6. 固定元素位置

position: fixedabsolute的元素,其重排影响范围较小(只在自己层内)。

7. 避免使用table布局

一个小改动可能触发整个table的重排。

六、实战:一个性能优化的例子

假设你要做一个跟随鼠标移动的小光点(类似鼠标特效)。错误做法:每帧改变top/left,触发重排。正确做法:用transform

// 差:每移动1px就重排一次
dot.style.left = x + 'px';
dot.style.top = y + 'px';

// 好:只触发合成
dot.style.transform = `translate(${x}px, ${y}px)`;

七、怎么分析页面重排/重绘?

Chrome DevTools → Performance 面板,录制一段操作,查看“Layout Shift”、“Paint”等标记。红色紫色区域越少越好。

八、总结:三兄弟的“饭量”

  • 重排:吃满汉全席,最贵。动几何、DOM结构。
  • 重绘:吃快餐,中等。动颜色、背景。
  • 合成:喝矿泉水,几乎免费。动transform、opacity。

优化口诀:能用transform别用left,能用class别改style,读写分离,批量操作。

如果你觉得今天的“三兄弟”够形象,点个赞让更多人看到。明天我们将聊聊JavaScript引擎与内存管理,看看V8是怎么给代码“打扫卫生”的。我们明天见!