4. 重排(Reflow)与重绘(Repaint):触发条件、性能开销对比,以及如何利用GPU加速与合成层优化

说到页面卡顿,有两个概念你肯定绕不开——重排和重绘。很多新手觉得这俩差不多,其实差远了。我见过不少项目,就因为没搞清楚它们的区别,随便改个样式就引发大面积重排,帧率直接掉到 20fps。

这一节,咱们就把它们彻底掰开揉碎。我会结合自己踩过的坑,告诉你什么操作会触发重排,什么操作只触发重绘,以及怎么用 GPU 和合成层来“白嫖”性能。

4.1 重排与重绘:到底谁更“贵”?

先给个直观结论:重排的代价,通常是重绘的 10 倍以上。为什么?

  • 重排(Reflow):浏览器要重新计算元素的几何位置、尺寸、布局。这就像你重新装修房子,得砸墙、改水电、铺地板,整个结构都得动。
  • 重绘(Repaint):只是重新画外观,比如改个颜色、换个背景图。相当于给墙刷个漆,结构不变。

你想想看,刷漆和砸墙,哪个更累?

对比维度 重排(Reflow) 重绘(Repaint)
触发条件 改变尺寸、位置、增删 DOM、读取布局属性 改变颜色、背景、阴影、边框样式
性能开销 极高(涉及布局计算 + 绘制) 较低(仅绘制,不涉及布局)
是否影响子元素 会,可能引发连锁重排 通常只影响自身
能否避免 尽量合并、减少触发 能避免就避免,但优先级低于重排

核心原则:优先避免重排,其次减少重绘。能用 transform 和 opacity 解决的问题,就别动 left/top 和 display。

4.2 哪些操作会触发重排?

我总结了一个“重排触发清单”,你在写代码时可以对照着检查:

  1. 改变几何属性:width、height、padding、margin、border、top、left、right、bottom。
  2. 改变盒模型:display: none(会触发重排)、visibility: hidden(只触发重绘)。
  3. 增删 DOM 节点:appendChild、removeChild、innerHTML 赋值。
  4. 改变内容:文本变化、图片尺寸变化、输入框 value 变化。
  5. 读取布局属性:offsetTop、offsetLeft、offsetWidth、offsetHeight、scrollTop、scrollLeft、clientTop、clientWidth、getComputedStyle()。
  6. 改变字体:font-size、font-family、line-height。
  7. 窗口 resize:浏览器窗口大小变化。

我曾经踩过的坑:在循环里同时读写 offsetHeight 和修改 style.height。每次读取都会强制触发一次重排,导致页面卡成 PPT。后来改成先批量读取,再批量写入,帧率直接从 15fps 飙到 60fps。

4.3 哪些操作只触发重绘?

这些属性只改变外观,不影响布局:

  • color、background-color、background-image
  • border-color、border-style(不改变 border-width)
  • outline-color、outline-style
  • box-shadow(不改变元素尺寸)
  • text-decoration、text-shadow
  • visibility(hidden 只重绘,visible 可能触发重排)

嗯,这里要注意:box-shadow 虽然只触发重绘,但如果阴影太大,绘制开销依然很高。我优化过一个页面,就是因为阴影叠加太多,GPU 都扛不住。

4.4 GPU 加速与合成层:性能优化的“核武器”

为什么有些动画特别流畅?比如用 transform: translate() 移动元素,或者用 opacity 做淡入淡出。因为它们触发了 合成层(Composite Layer)。

说白了,浏览器会把页面分成多个图层。普通元素在同一个图层里,改个 left 就得重排整个图层。但如果你把元素提升到独立图层,浏览器就可以让 GPU 单独处理它,完全不打扰其他元素。

4.4.1 如何创建合成层?

以下属性会让元素独立成层:

  • transform: translateZ(0) 或 translate3d(0,0,0)
  • will-change: transform 或 will-change: opacity
  • opacity(配合动画时)
  • position: fixed
  • video、canvas、iframe 等原生元素

我的个人习惯:在做动画前,先给目标元素加一个 will-change: transform,告诉浏览器“我要动它了,提前准备好图层”。用完记得移除,不然会浪费内存。

4.4.2 合成层的优势

  • 不触发重排:transform 和 opacity 的变化,完全由 GPU 处理,不涉及布局计算。
  • 不触发重绘:GPU 直接合成,连绘制阶段都跳过了。
  • 60fps 稳稳的:只要不滥用,合成层动画几乎不会掉帧。

4.5 知识体系图:重排、重绘与合成层的关系

下面这张图,是我自己梳理的。它把整个流程串起来了,你看完应该能明白:为什么 transform 是性能优化的“亲儿子”。

重排、重绘与合成层关系图 用户操作 / JS 修改 是否改变布局? 是 重排(Reflow) 否 重绘(Repaint) 重排必然触发重绘 是否在合成层? 是 GPU 合成 否 CPU 普通绘制 屏幕显示(60fps) transform / opacity 直接走 GPU 合成

4.6 实战优化策略

光知道理论不够,得能落地。我分享几个我在项目中用过的优化手段:

4.6.1 批量读写,避免强制重排

// ❌ 坏写法:每次读取都触发重排
for (let i = 0; i < items.length; i++) {
  let height = items[i].offsetHeight;  // 强制重排
  items[i].style.height = height + 10 + 'px';  // 再次重排
}

// ✅ 好写法:先批量读取,再批量写入
let heights = [];
for (let i = 0; i < items.length; i++) {
  heights.push(items[i].offsetHeight);  // 批量读取
}
for (let i = 0; i < items.length; i++) {
  items[i].style.height = heights[i] + 10 + 'px';  // 批量写入
}

4.6.2 用 transform 代替 left/top

// ❌ 触发重排
element.style.left = '100px';
element.style.top = '200px';

// ✅ 只触发合成,不触发重排
element.style.transform = 'translate(100px, 200px)';

4.6.3 使用 will-change 提前告知浏览器

/* 提前告诉浏览器,这个元素要动 */
.animated-element {
  will-change: transform, opacity;
}

注意:will-change 不要滥用。我见过有人给页面里所有元素都加上了 will-change,结果内存暴涨,反而更卡。只给真正需要动画的元素加,动画结束后记得移除。

4.6.4 使用 requestAnimationFrame 做动画

function animate() {
  element.style.transform = `translateX(${pos}px)`;
  pos += 1;
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

为什么用 requestAnimationFrame?因为它会在浏览器下一次重绘之前执行,保证动画和屏幕刷新率同步。用 setInterval 的话,很容易出现掉帧或撕裂。

4.7 总结一下

重排和重绘,说白了就是浏览器在“干活”。重排是干重活,重绘是干轻活。而合成层,就是让 GPU 帮你干最轻松的活。

我个人习惯是:能用 transform 和 opacity 解决的,绝不动布局属性。这个习惯帮我省掉了无数次性能优化的返工。你也不妨试试。