跳到主要内容

重绘与重排(Repaint & Reflow)

在前端性能优化中,重绘(Repaint)重排(Reflow) 是两个非常核心的概念。理解它们的触发机制和优化手段,不仅能帮助我们写出更高性能的页面,也是前端面试中的高频考点。

浏览器渲染流程

在讨论重绘和重排之前,我们需要先了解浏览器是怎样把 HTML、CSS 和 JavaScript 变成用户看到的页面的。整个流程可以用"流水线"来理解:

每一步做的事情:

步骤英文名做了什么
1. 构建 DOM 树Parse HTML把 HTML 标签解析成一棵节点树
2. 构建 CSSOM 树Parse CSS把 CSS 规则解析成样式树
3. 生成渲染树Render Tree将 DOM 和 CSSOM 合并,剔除不可见元素(如 display: none
4. 布局Layout / Reflow计算每个节点在页面中的位置大小
5. 绘制Paint / Repaint将节点的视觉样式(颜色、阴影、边框等)转化为像素
6. 合成Composite将不同图层合并,最终显示在屏幕上
类比理解

想象你在装修房子:布局就是决定每面墙在哪里、房间多大(结构);绘制就是给墙刷漆、贴壁纸(外观);合成就是把所有房间的效果整合起来,形成完整的家。

什么是重排(Reflow)

重排也叫回流,指的是当页面的布局信息发生变化时,浏览器需要重新计算元素的几何属性(位置、大小),然后重新进行布局的过程。

简单理解:任何影响元素"在哪里"和"多大"的改变,都会触发重排。

重要

重排一定会触发重绘,但重绘不一定触发重排。重排的代价比重绘高得多,因为它要从布局阶段重新开始走完整个后续流程。

触发重排的常见操作

1. 修改几何属性

/* 以下属性的修改都会触发重排 */
.box {
width: 200px; /* 宽度 */
height: 100px; /* 高度 */
padding: 10px; /* 内边距 */
margin: 20px; /* 外边距 */
border-width: 2px; /* 边框宽度 */
top: 50px; /* 定位偏移 */
left: 100px;
font-size: 16px; /* 字体大小也会影响布局 */
}

2. DOM 结构变化

// 添加或删除可见元素
document.body.appendChild(newElement);
document.body.removeChild(oldElement);

// 修改元素内容(可能改变尺寸)
element.innerText = '新的文本内容,可能比原来更长';

3. 读取布局信息

这一点容易被忽略 —— 读取某些属性也会触发重排!因为浏览器必须确保返回最新的、准确的值:

// 以下属性/方法的访问都会强制触发重排
element.offsetTop;
element.offsetLeft;
element.offsetWidth;
element.offsetHeight;
element.scrollTop;
element.scrollLeft;
element.scrollWidth;
element.scrollHeight;
element.clientTop;
element.clientLeft;
element.clientWidth;
element.clientHeight;

// 方法
element.getBoundingClientRect();
window.getComputedStyle(element);
为什么读取也会触发重排?

浏览器通常会将多次 DOM 修改"攒"在一起,放入一个队列中批量处理(这叫做批量异步重排)。但当你读取布局属性时,浏览器被迫立刻清空这个队列,执行重排以确保你拿到的值是最新的。这就是所谓的强制同步布局(Forced Synchronous Layout)

4. 窗口和全局变化

// 窗口大小变化
window.addEventListener('resize', handler);

// 字体加载完成(可能改变文字尺寸)
document.fonts.ready.then(() => { /* 可能触发重排 */ });

// 激活 CSS 伪类(如 :hover 改变了尺寸)

重排触发属性速查表

类别属性
盒模型widthheightpaddingmarginborder-width
定位positiontoprightbottomleftfloatclear
文字font-sizefont-weightfont-familyline-heighttext-alignwhite-space
显示displayoverflowvertical-align
弹性/网格flexgrid-template-columnsgap

什么是重绘(Repaint)

重绘指的是当元素的外观样式(颜色、背景、阴影等)发生变化,但不影响布局时,浏览器只需要重新"涂色"的过程。

简单理解:只改"长什么样"、不改"在哪里"和"多大",就只触发重绘。

仅触发重绘的常见属性

/* 这些属性的修改只触发重绘,不触发重排 */
.box {
color: red; /* 文字颜色 */
background-color: blue; /* 背景色 */
background-image: url(); /* 背景图 */
border-color: green; /* 边框颜色 */
border-style: dashed; /* 边框样式 */
border-radius: 10px; /* 圆角 */
outline: 1px solid red; /* 轮廓线 */
box-shadow: 0 0 5px #000;/* 阴影 */
visibility: hidden; /* 隐藏(仍占位) */
text-decoration: underline; /* 文本装饰 */
}
visibility 和 display 的区别
  • visibility: hidden → 元素不可见但仍然占据空间,只触发重绘
  • display: none → 元素从渲染树中移除,不占空间,触发重排

重绘与重排的对比

对比项重排(Reflow)重绘(Repaint)
触发条件几何属性变化(位置、大小)外观样式变化(颜色、阴影)
涉及阶段Layout → Paint → CompositePaint → Composite
性能开销(需重新计算布局)(只重新绘制像素)
影响范围可能波及父元素和兄弟元素通常只影响当前元素
是否包含对方重排一定包含重绘重绘不一定触发重排

性能优化策略

了解了重绘和重排的原理后,我们来看如何在实际开发中减少它们的发生。

1. 批量修改样式

// ❌ 差:每次修改都可能触发一次重排
element.style.width = '100px';
element.style.height = '200px';
element.style.margin = '10px';

// ✅ 好:通过 cssText 一次性修改
element.style.cssText = 'width: 100px; height: 200px; margin: 10px;';

// ✅ 好:通过切换 class 一次性修改
element.className = 'new-style';
// 或者
element.classList.add('new-style');

2. 避免逐项读写交替

// ❌ 差:读写交替,每次读取都强制触发重排
const width = element.offsetWidth; // 读 → 触发重排
element.style.width = width + 10 + 'px'; // 写
const height = element.offsetHeight; // 读 → 再次触发重排
element.style.height = height + 10 + 'px'; // 写

// ✅ 好:先集中读取,再集中写入
const width = element.offsetWidth; // 读
const height = element.offsetHeight; // 读(同一"干净"状态,不会再次重排)
element.style.width = width + 10 + 'px'; // 写
element.style.height = height + 10 + 'px'; // 写(浏览器批量处理)
核心原则

读写分离 —— 把所有的"读取布局信息"操作集中在一起,把所有的"修改样式"操作集中在一起,避免交替进行。

3. 使用 DocumentFragment 批量操作 DOM

// ❌ 差:每次插入都可能触发重排
for (let i = 0; i < 100; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
list.appendChild(li); // 每次都可能重排
}

// ✅ 好:先在 Fragment 中组装,一次性插入
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li); // Fragment 不在 DOM 中,不会触发重排
}
list.appendChild(fragment); // 只触发一次重排

4. 离线 DOM 操作

将元素从渲染流中取出,修改完成后再放回:

// 方法一:先隐藏,修改完再显示
element.style.display = 'none'; // 触发一次重排
// ... 进行大量修改 ...
element.style.display = 'block'; // 触发一次重排
// 总共只有 2 次重排,而不是 N 次

// 方法二:克隆节点,替换回去
const clone = element.cloneNode(true);
// ... 对 clone 进行大量修改 ...
element.parentNode.replaceChild(clone, element); // 只触发一次重排

5. 使用 transform 和 opacity 做动画

transformopacity 是两个特殊属性,它们的变化只触发合成(Composite)阶段,跳过了布局和绘制,性能最好:

/* ❌ 差:使用 top/left 做动画 → 每帧都触发重排 */
.move-bad {
transition: top 0.3s, left 0.3s;
position: absolute;
top: 0;
left: 0;
}
.move-bad:hover {
top: 100px;
left: 100px;
}

/* ✅ 好:使用 transform 做动画 → 只触发合成,GPU 加速 */
.move-good {
transition: transform 0.3s;
}
.move-good:hover {
transform: translate(100px, 100px);
}
为什么 transform 和 opacity 这么快?

因为它们可以在合成器线程(Compositor Thread) 上独立处理,不需要经过主线程。浏览器会为使用了这些属性的元素创建独立的合成层(Compositing Layer),由 GPU 直接处理,所以非常高效。

6. 使用 will-change 提升合成层

/* 提前告知浏览器该元素即将变化,让浏览器做好优化准备 */
.animated-box {
will-change: transform, opacity;
}
注意

will-change 不能滥用!每个合成层都会消耗额外的内存。只在确实需要动画的元素上使用,动画结束后最好移除。

7. 使用 requestAnimationFrame

// ❌ 差:在任意时刻修改样式,可能导致掉帧
setTimeout(() => {
element.style.transform = 'translateX(100px)';
}, 16);

// ✅ 好:在浏览器下一次重绘前执行,与刷新率同步
requestAnimationFrame(() => {
element.style.transform = 'translateX(100px)';
});

优化策略总结

策略原理适用场景
批量修改样式减少重排次数需要同时修改多个样式
读写分离避免强制同步布局需要读取布局信息后再修改
DocumentFragment离线 DOM 操作批量插入大量节点
display: none 先隐藏再操作离线 DOM 操作对某个元素进行大量修改
transform / opacity 做动画跳过布局和绘制位移、缩放、旋转、淡入淡出
will-change提前创建合成层即将发生动画的元素
requestAnimationFrame与浏览器刷新同步JavaScript 驱动的动画

实战示例:性能对比

下面的例子演示了"如何将 1000 个元素移动到新位置"在优化前后的区别:

// ❌ 未优化版本:频繁触发重排
function moveElementsBad(elements) {
elements.forEach((el) => {
const currentTop = el.offsetTop; // 读 → 强制重排
el.style.top = currentTop + 10 + 'px'; // 写
});
}

// ✅ 优化版本:读写分离
function moveElementsGood(elements) {
// 第一步:集中读取所有位置
const positions = elements.map((el) => el.offsetTop);

// 第二步:集中写入所有新位置
elements.forEach((el, i) => {
el.style.top = positions[i] + 10 + 'px';
});
}

// ✅ 最优版本:使用 transform,完全避免重排和重绘
function moveElementsBest(elements) {
elements.forEach((el) => {
el.style.transform = 'translateY(10px)';
});
}

如何检测重绘和重排

在 Chrome DevTools 中可以直观地观察重绘和重排:

1. Performance 面板

  1. 打开 DevTools → Performance 面板
  2. 点击录制按钮,操作页面
  3. 停止录制,查看时间线中的 Layout(重排)和 Paint(重绘)事件

2. Rendering 面板

  1. 打开 DevTools → 按 Esc 打开底部抽屉 → 选择 Rendering
  2. 勾选 Paint flashing:页面中发生重绘的区域会闪绿色
  3. 勾选 Layout Shift Regions:发生布局偏移的区域会闪蓝色

3. Performance Monitor

  1. 打开 DevTools → Ctrl + Shift + P(Mac: Cmd + Shift + P
  2. 输入 "Performance Monitor" 并选择
  3. 实时监控 Layouts / secStyle recalcs / sec

面试高频问答

Q1:什么是重绘和重排?它们有什么区别?

答:

  • 重排(Reflow) 是指页面布局信息发生变化时,浏览器重新计算元素的几何属性(位置、大小)的过程。比如修改了 widthheightmargin 等属性。
  • 重绘(Repaint) 是指元素的外观样式发生变化但不影响布局时,浏览器重新绘制元素像素的过程。比如修改了 colorbackground-color 等属性。
  • 核心区别:重排一定会导致重绘,但重绘不一定触发重排。重排的性能开销远大于重绘。

Q2:为什么读取 offsetWidth 等属性会触发重排?

答: 浏览器通常会将多次 DOM 修改放入队列中批量处理。但当你读取 offsetWidthoffsetHeightgetBoundingClientRect() 等布局属性时,浏览器为了保证返回准确的值,必须立即清空队列、执行所有挂起的修改并进行重排。这就是强制同步布局(Forced Synchronous Layout)。所以在代码中应避免读写交替,遵循"先读后写"的原则。

Q3:如何减少重排和重绘?请列举至少 3 种优化手段。

答:

  1. 批量修改样式:使用 cssText 或切换 className 一次性修改多个样式,避免逐条修改。
  2. 读写分离:先集中读取所有布局属性,再集中写入修改,避免强制同步布局。
  3. 使用 DocumentFragment:批量 DOM 操作时,先在 Fragment 中组装,最后一次性插入真实 DOM。
  4. 使用 transform/opacity 做动画:这两个属性只触发合成阶段,跳过布局和绘制,性能最好。
  5. 离线 DOM 操作:通过 display: none 隐藏元素后再批量修改,或使用克隆节点。

Q4:为什么推荐使用 transform 而不是 top/left 做动画?

答: 使用 top/left 做动画,每一帧都会触发重排(Layout)→ 重绘(Paint)→ 合成(Composite)的完整流程。而 transform 属性的变化只触发合成(Composite)阶段,浏览器会为该元素创建独立的合成层,由 GPU 直接处理,不经过主线程,因此性能大幅提升,动画也更流畅。

Q5:visibility: hiddendisplay: none 对重排重绘的影响有什么不同?

答:

  • display: none:元素从渲染树中完全移除,不占据任何空间。设置或取消都会触发重排 + 重绘
  • visibility: hidden:元素不可见但仍然占据空间,只是视觉上看不到了。设置或取消只会触发重绘,不会触发重排。
  • 因此在需要频繁切换显隐的场景下,visibility: hidden 的性能优于 display: none

Q6:什么是合成层(Compositing Layer)?哪些情况会创建合成层?

答: 合成层是浏览器为某些元素单独分配的 GPU 图层。合成层上的变化只需在合成阶段处理,不需要重排和重绘,性能极高。以下情况会创建合成层:

  • 使用了 transform 的 3D 变换(如 translate3dtranslateZ
  • 设置了 will-change: transformwill-change: opacity
  • 使用了 <video><canvas><iframe> 等元素
  • 使用了 CSS animationtransition 且作用于 opacity/transform
  • 元素与已有合成层重叠(隐式合成)

但合成层越多,内存消耗越大,所以不能滥用。