4. 重排(Reflow)与重绘(Repaint):触发条件、性能开销对比,以及如何利用GPU加速与合成层优化
说到页面卡顿,有两个概念你肯定绕不开——重排和重绘。很多新手觉得这俩差不多,其实差远了。我见过不少项目,就因为没搞清楚它们的区别,随便改个样式就引发大面积重排,帧率直接掉到 20fps。
这一节,咱们就把它们彻底掰开揉碎。我会结合自己踩过的坑,告诉你什么操作会触发重排,什么操作只触发重绘,以及怎么用 GPU 和合成层来“白嫖”性能。
4.1 重排与重绘:到底谁更“贵”?
先给个直观结论:重排的代价,通常是重绘的 10 倍以上。为什么?
- 重排(Reflow):浏览器要重新计算元素的几何位置、尺寸、布局。这就像你重新装修房子,得砸墙、改水电、铺地板,整个结构都得动。
- 重绘(Repaint):只是重新画外观,比如改个颜色、换个背景图。相当于给墙刷个漆,结构不变。
你想想看,刷漆和砸墙,哪个更累?
| 对比维度 | 重排(Reflow) | 重绘(Repaint) |
|---|---|---|
| 触发条件 | 改变尺寸、位置、增删 DOM、读取布局属性 | 改变颜色、背景、阴影、边框样式 |
| 性能开销 | 极高(涉及布局计算 + 绘制) | 较低(仅绘制,不涉及布局) |
| 是否影响子元素 | 会,可能引发连锁重排 | 通常只影响自身 |
| 能否避免 | 尽量合并、减少触发 | 能避免就避免,但优先级低于重排 |
核心原则:优先避免重排,其次减少重绘。能用 transform 和 opacity 解决的问题,就别动 left/top 和 display。
4.2 哪些操作会触发重排?
我总结了一个“重排触发清单”,你在写代码时可以对照着检查:
- 改变几何属性:width、height、padding、margin、border、top、left、right、bottom。
- 改变盒模型:display: none(会触发重排)、visibility: hidden(只触发重绘)。
- 增删 DOM 节点:appendChild、removeChild、innerHTML 赋值。
- 改变内容:文本变化、图片尺寸变化、输入框 value 变化。
- 读取布局属性:offsetTop、offsetLeft、offsetWidth、offsetHeight、scrollTop、scrollLeft、clientTop、clientWidth、getComputedStyle()。
- 改变字体:font-size、font-family、line-height。
- 窗口 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: opacityopacity(配合动画时)position: fixedvideo、canvas、iframe等原生元素
我的个人习惯:在做动画前,先给目标元素加一个 will-change: transform,告诉浏览器“我要动它了,提前准备好图层”。用完记得移除,不然会浪费内存。
4.4.2 合成层的优势
- 不触发重排:transform 和 opacity 的变化,完全由 GPU 处理,不涉及布局计算。
- 不触发重绘:GPU 直接合成,连绘制阶段都跳过了。
- 60fps 稳稳的:只要不滥用,合成层动画几乎不会掉帧。
4.5 知识体系图:重排、重绘与合成层的关系
下面这张图,是我自己梳理的。它把整个流程串起来了,你看完应该能明白:为什么 transform 是性能优化的“亲儿子”。
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 解决的,绝不动布局属性。这个习惯帮我省掉了无数次性能优化的返工。你也不妨试试。