重排和重绘
更新: 9/11/2026字数: 0 字 时长: 0 分钟
在前端性能优化中,重排(Reflow,也称回流)和重绘(Repaint)是浏览器渲染页面时最核心的两个概念。搞懂它们的触发机制和优化策略,是写出流畅页面的关键。
一、 核心概念与区别
浏览器渲染页面的大致流程为:HTML/CSS 解析 → 生成 DOM/CSSOM 树 → 构建渲染树(Render Tree) → 布局/重排(Layout/Reflow) → 绘制/重绘(Paint/Repaint) → 合成(Composite)。
| 概念 | 含义 | 触发原因 | 性能开销 |
|---|---|---|---|
| 重排 (Reflow) | 浏览器重新计算元素的几何属性(位置、大小),并重新构建渲染树的一部分或全部。 | 元素的几何尺寸、位置、显示状态(如 display)、页面结构发生变化。 | 极高(引发链式反应,影响祖先与相邻元素) |
| 重绘 (Repaint) | 浏览器重新绘制元素的外观,但不影响其几何布局。 | 元素的外观样式发生变化(如 color、background-color)。 | 较低(只需重新填充像素点) |
关键规律:重排一定会触发重绘,但重绘不一定会触发重排。
二、 哪些操作会触发重排与重绘?
1. 触发重排(Reflow)的操作
- 页面首次渲染(无法避免)。
- DOM 结构改变:添加、删除、移动 DOM 节点。
- 元素几何属性变化:修改
width、height、padding、margin、border等。 - 位置与显示变化:修改
top、left、display: none(注:visibility: hidden只触发重绘)。 - 字体与文本变化:修改
font-size、text-align或用户输入内容。 - 调整浏览器窗口大小:触发
resize事件。 - 读取特定的 DOM 属性或调用 API(强制刷新渲染队列):
- 偏移量及尺寸:
offsetTop/offsetLeft/offsetWidth/offsetHeight - 滚动属性:
scrollTop/scrollLeft/scrollWidth/scrollHeight - 客户区尺寸:
clientTop/clientLeft/clientWidth/clientHeight - 调用 API:
getComputedStyle()、getBoundingClientRect()
2. 仅触发重绘(Repaint)的操作
修改不涉及几何属性的外观特征:
color、background-color、border-stylevisibility: hiddenbox-shadow、outline
三、 性能优化实战技巧
为了避免页面卡顿(丢帧),日常开发中应尽量减少重排和重绘的次数:
1. CSS 优化策略
- 使用
transform代替top/left做动画:transform和opacity会开启硬件加速(GPU 合成),只触发 Composite(合成) 阶段,既不重排也不重绘。 - **使用
visibility: hidden代替display: none**:前者只重绘,后者会触发重排。 - 批量修改样式:使用
class类名一次性改变样式,而不是频繁写element.style.xxx。 - 离屏渲染:设置
position: absolute或fixed使动画元素脱离文档流,使其重排不影响其他普通文档流节点。
2. JS / DOM 操作优化策略
- DOM 批量更新(DocumentFragment / 离屏):
javascript
// ❌ 差的做法:循环 1000 次,触发 1000 次重排
for (let i = 0; i < 1000; i++) {
const el = document.createElement('div');
document.body.appendChild(el);
}
// ✅ 好的做法:使用 DocumentFragment 一次性插入
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const el = document.createElement('div');
fragment.appendChild(el);
}
document.body.appendChild(fragment); // 仅触发 1 次重排- 避免强制同步布局(Forced Synchronous Layout): 避免在循环中先写入 DOM,再读取位置属性。
javascript
// ❌ 差的做法:读写交替,强制浏览器在循环中频繁重排
for (let i = 0; i < box.length; i++) {
box[i].style.width = container.offsetWidth + 'px'; // 读取引发强制重排,紧接着写入
}
// ✅ 好的做法:读写分离
const width = container.offsetWidth; // 先统一读取
for (let i = 0; i < box.length; i++) {
box[i].style.width = width + 'px'; // 再统一写入
}- **使用
requestAnimationFrame**:将频繁的 DOM 读写操作放到下一帧渲染前统一执行。