4. 重排与重绘:触发条件、性能开销对比,以及如何避免强制同步布局
说到页面流畅度,有两个概念你肯定绕不开——重排和重绘。
我见过不少开发者,一提到性能优化就只知道「减少重排重绘」。但真要问他们:什么操作会触发重排?重排和重绘到底谁更贵?怎么才能避免强制同步布局?很多人就答不上来了。
这一节,咱们就把这些事彻底掰扯清楚。
4.1 先搞清楚:浏览器是怎么渲染一帧的?
在讲重排重绘之前,你得先理解浏览器渲染一帧的流水线。我习惯把它分成五个步骤:
- JavaScript:执行脚本,修改 DOM 或 CSSOM
- Style:计算元素的最终样式
- Layout:计算元素的位置和尺寸(这就是重排)
- Paint:绘制像素(这就是重绘)
- Composite:合成图层,显示到屏幕上
说白了,重排一定会触发重绘,但重绘不一定需要重排。举个例子:你只改了个背景色,浏览器不需要重新计算布局,直接画就行。但你要是改了宽度或高度,那布局就得重新算,然后重新画。
核心结论:重排的性能开销远大于重绘。一次重排可能引发整个子树甚至整个文档的布局重新计算。
4.2 哪些操作会触发重排?
我在项目中踩过不少坑,总结下来,触发重排的操作大致分这几类:
| 类别 | 具体操作 | 示例 |
|---|---|---|
| 尺寸变化 | 修改 width、height、padding、margin、border | el.style.width = '200px' |
| 位置变化 | 修改 top、left、right、bottom | el.style.top = '50px' |
| 内容变化 | 修改文本、图片尺寸、输入框值 | el.textContent = '新内容' |
| DOM 操作 | 增删元素、修改 class、切换 display | document.body.appendChild(div) |
| 读取布局属性 | 读取 offsetTop、scrollHeight、getComputedStyle | el.offsetHeight |
| 窗口变化 | resize 事件 | window.addEventListener('resize', ...) |
嗯,这里要注意最后一条——读取布局属性。这是最容易忽略的坑。你想想看,如果你先改了样式,然后又立刻去读布局属性,浏览器为了给你「准确」的值,只能强制把队列里的布局任务提前执行。这就是所谓的强制同步布局。
避坑指南:我曾经在一个列表动画里,每帧都去读 offsetTop,结果页面直接卡成 PPT。后来才发现,每次读取都触发了强制同步布局,性能直接崩了。
4.3 重排 vs 重绘:性能开销到底差多少?
我直接给你一个直观的对比:
| 对比维度 | 重排(Layout) | 重绘(Paint) |
|---|---|---|
| 触发条件 | 尺寸、位置、内容、DOM 结构变化 | 颜色、背景、阴影、边框样式变化 |
| 影响范围 | 可能波及整个文档 | 通常只影响当前图层 |
| 计算复杂度 | O(n) 或更高(n 为元素数量) | O(像素面积) |
| 典型耗时 | 几毫秒到几十毫秒 | 亚毫秒到几毫秒 |
| 能否避免 | 尽量减少,无法完全避免 | 可以大量减少 |
说白了,一次重排的代价可能是重绘的 10 倍甚至更多。所以优化的时候,优先减少重排。
4.4 如何避免强制同步布局?
这是实战中最常见的坑。我总结了一套「读写分离」的策略:
4.4.1 批量读取,批量写入
不要写一行读一行。把所有的写操作集中在一起,读操作也集中在一起。
// ❌ 错误写法:读写交替,触发强制同步布局
el.style.width = '100px';
const height = el.offsetHeight; // 强制布局
el.style.height = height + 50 + 'px'; // 再次触发布局
// ✅ 正确写法:先批量读,再批量写
const height = el.offsetHeight; // 先读
el.style.width = '100px';
el.style.height = height + 50 + 'px'; // 再写
4.4.2 使用 requestAnimationFrame 做读写分离
如果读写操作必须分散在不同的事件里,可以用 requestAnimationFrame 把写操作推迟到下一帧。
// 在事件回调里只读
function onScroll() {
const scrollTop = window.scrollY;
// 把写操作推迟到下一帧
requestAnimationFrame(() => {
el.style.transform = `translateY(${scrollTop}px)`;
});
}
4.4.3 用 transform 代替 top/left
这个技巧我几乎在每个项目里都用。修改 transform 不会触发重排,只会触发合成。
// ❌ 触发重排
el.style.top = '100px';
el.style.left = '200px';
// ✅ 只触发合成
el.style.transform = 'translate(200px, 100px)';
小技巧:如果你要做动画,优先用 transform 和 opacity。这两个属性只会触发合成,性能最好。我习惯把动画元素单独提升为一个图层,用 will-change: transform 告诉浏览器提前准备。
4.5 知识体系总览
下面这张图帮你把这一节的核心逻辑串起来:
4.6 实战建议
最后,给你几个我平时写代码时一定会遵守的原则:
- 能用 transform 就别用 top/left——这个习惯养成了,动画性能基本不会差。
- 批量操作 DOM——用
DocumentFragment或者先把元素隐藏再操作,最后一次性显示。 - 避免频繁读取布局属性——如果一定要读,先缓存下来。
- 使用 will-change 要克制——别给所有元素都加上,否则浏览器会提前分配大量资源,反而拖慢性能。
嗯,这一节的内容就到这里。记住一句话:重排是性能的敌人,读写分离是你的武器。下次遇到卡顿,先检查是不是触发了强制同步布局。