重排(Reflow)与重绘(Repaint)的区别?以及如何减少重排和重绘?
重排(Reflow)与重绘(Repaint):前端性能优化的基石
在网页开发中,我们经常听到“重排”(Reflow)和“重绘”(Repaint)这两个术语。它们是浏览器渲染页面的核心过程,也是影响页面性能的关键因素。理解它们的区别和触发条件,是进行前端性能优化的基础。
一、浏览器渲染流程简述
在深入重排和重绘之前,先简单回顾一下浏览器渲染页面的基本流程:
- 解析 HTML:生成 DOM 树。
- 解析 CSS:生成 CSSOM 树。
- 合并 DOM 和 CSSOM:生成渲染树(Render Tree)。
- 布局(Layout):计算渲染树中每个节点的几何位置(坐标、尺寸等)。
- 绘制(Painting):将渲染树的每个节点转换为屏幕上的像素。
- 合成(Compositing):将多个图层合并成最终图像显示在屏幕上。
其中,布局对应的就是重排,绘制对应的就是重绘。
二、什么是重排(Reflow)?
重排(也称“回流”),指的是浏览器为了重新计算 DOM 元素的几何属性(如位置、大小),从而需要更新渲染树和布局的过程。
本质:重新计算元素的几何信息。
影响范围:
- 局部重排:只影响部分 DOM 结构。例如,修改一个
div
的宽度,可能只导致该div
及其后代元素重新布局。 - 全局重排:影响整个页面的布局。例如,修改
<html>
根元素的字体大小,可能导致所有文本元素都需要重新计算尺寸和位置,从而引发全局重排。全局重排成本极高。
- 局部重排:只影响部分 DOM 结构。例如,修改一个
触发重排的操作示例:
- 添加、删除、修改 DOM 节点。
- 改变元素的几何属性:
width
,height
,padding
,margin
,border
,top
,left
等。 - 改变浏览器窗口大小(
resize
事件)。 - 读取某些会触发布局计算的属性(见下文)。
- 访问
offsetTop
,offsetLeft
,offsetWidth
,offsetHeight
,scrollTop
,scrollLeft
,clientWidth
,clientHeight
,getComputedStyle()
等。注意:这些属性的读取本身就会强制浏览器立即执行重排以获取最新值,这被称为“强制同步布局”(Forced Synchronous Layout),是性能杀手。
三、什么是重绘(Repaint)?
重绘,指的是当元素的外观(如颜色、背景、边框样式等)发生变化,但几何属性没有改变时,浏览器需要重新绘制该元素的过程。
本质:重新绘制元素的视觉外观。
影响范围:通常只影响元素本身及其视觉层叠相关的区域。
成本:重绘的成本远低于重排,因为它跳过了计算布局的昂贵步骤。
触发重绘的操作示例:
- 改变元素的颜色:
color
。 - 改变背景色或背景图片:
background-color
,background-image
。 - 改变边框颜色或可见性:
border-color
,visibility
(visibility: hidden
会重绘,但元素仍占据空间)。 - 改变文本样式:
font-weight
,text-decoration
等。 - 注意:
display: none
会触发重排(因为它移除了元素,改变了布局),而visibility: hidden
只触发重绘。
- 改变元素的颜色:
四、关键区别与关系
特性 | 重排 (Reflow) | 重绘 (Repaint) |
---|---|---|
触发原因 | 几何属性改变(位置、尺寸) | 外观属性改变(颜色、背景等) |
执行步骤 | 更新渲染树 → 布局 (Layout) → 绘制 (Painting) → 合成 | 更新渲染树 → 绘制 (Painting) → 合成 |
性能成本 | 非常高 | 相对较低 |
是否包含 | 必然包含重绘 | 不包含重排 |
影响范围 | 可能影响局部或全局布局 | 通常影响局部视觉 |
核心关系:重排一定会导致重绘,但重绘不一定导致重排。
想象一下:你修改了一个盒子的宽度(重排),它的位置和大小都变了,那么它当然需要被重新画出来(重绘)。但如果你只是把盒子从红色改成蓝色(重绘),它的位置和大小没变,就不需要重新计算布局(无重排)。
五、如何减少重排和重绘?
批量修改 DOM:
- 使用
DocumentFragment
创建一个临时容器,在容器内完成所有 DOM 操作,然后一次性插入到 DOM 树中。 - 或者先将元素
display: none
(触发一次重排),进行多次修改,再恢复display
(再触发一次重排),避免中间过程的多次重排。
- 使用
避免“强制同步布局”:
- 不要在修改样式后立即读取
offsetTop
等布局属性。将读写操作分开,先完成所有写操作,再统一读取。
// ❌ 错误:反复触发重排 for (let i = 0; i < items.length; i++) { items[i].style.width = items[i].offsetWidth + 10 + 'px'; // 读写交替 } // ✅ 正确:先读取,再修改 const widths = items.map(item => item.offsetWidth); requestAnimationFrame(() => { items.forEach((item, i) => { item.style.width = widths[i] + 10 + 'px'; }); });
- 不要在修改样式后立即读取
使用 CSS 类名代替直接修改样式:
- 将多条 CSS 规则定义在一个类中,通过
classList.add/remove/toggle
来切换,而不是逐个修改style
属性。
- 将多条 CSS 规则定义在一个类中,通过
使用
transform
和opacity
实现动画:- 这两个属性可以触发合成层(Compositing Layer),浏览器可以在独立的图层上进行 GPU 加速处理,通常只涉及合成(Compositing),既不重排也不重绘,性能最佳。
优化 CSS 选择器:
- 避免使用过于复杂或低效的 CSS 选择器,减少样式计算时间。
六、总结
重排和重绘是浏览器渲染性能的“双刃剑”。频繁的重排是导致页面卡顿、动画不流畅的罪魁祸首。作为前端开发者,我们需要深刻理解它们的原理和触发条件,并在实践中采取有效的策略来最小化它们的发生。通过合理使用 DocumentFragment
、避免强制同步布局、利用 CSS 类和 transform/opacity
等技巧,我们可以显著提升应用的响应速度和用户体验。记住:能不重排就不重排,能不重绘就不重绘,这是性能优化的金科玉律。