人人都会AI编程

18.2 重排(回流)与重绘

更新时间:2026-07-11

在 18.1 节中,我们了解了浏览器从接收 HTML 到绘制像素的完整渲染流水线。而真实的 Web 页面是动态的——用户交互、数据更新、动画效果都会导致页面发生变化。浏览器处理这些变化时,会触发两种代价不同的更新机制:重绘重排(又称回流)。理解它们的区别和触发条件,是写出高性能页面交互的第一步。

18.2.1 什么是重绘(Repaint)

重绘是指元素的视觉外观发生变化,但不影响布局时,浏览器重新绘制该元素的过程。常见的触发场景包括:

  • 修改 colorbackground-colorborder-colorbox-shadow 等视觉样式
  • 修改 visibility(注意不是 display
  • 修改 outlinetext-decoration 等不影响几何尺寸的属性

重绘并不需要重新计算元素的几何位置和大小(即不需要布局阶段),浏览器可以直接在已生成的渲染树上进行像素级的重新绘制。所以重绘的开销相对较小,但也不是零成本。绘制面积越大、视觉复杂度越高(如多层阴影、渐变),重绘所需时间越长。

18.2.2 什么是重排(Reflow)

重排是指元素的几何属性发生变化,导致浏览器必须重新计算页面布局,然后重新绘制受影响部分的过程。重排几乎总会触发后续的重绘,而比单纯的重绘昂贵得多。

重排的触发条件非常广泛,日常开发中不小心就会踩中:

修改影响几何尺寸的 CSS 属性

  • widthheightpaddingmarginborder
  • positiontopleftrightbottom
  • display(从 none 到其他值会触发重建,从其他值到 none 也会)
  • font-sizetext-align

操作 DOM

  • 增加、删除、移动 DOM 节点
  • 改变 DOM 的结构顺序

读取某些布局属性

浏览器为了优化性能,通常会将多个重排操作积攒起来一次性执行。但如果代码中穿插读取以下强制布局属性,浏览器就必须马上(同步地)执行队列中的重排,以确保返回准确值。这被称为强制同步布局(Forced Synchronous Layout)

常见的强制布局属性包括:

  • offsetTopoffsetLeftoffsetWidthoffsetHeight
  • scrollTopscrollLeftscrollWidthscrollHeight
  • clientTopclientLeftclientWidthclientHeight
  • getComputedStyle()getBoundingClientRect()

窗口尺寸变化

  • 浏览器窗口缩放,会触发全局重排

内容变化

  • 输入框文字改变、图片加载完成等可能影响尺寸的变化

伪类激活

  • 例如 :hover 改变了盒子尺寸

18.2.3 重排与重绘的性能影响

重排比重绘的代价高,是因为重排需要从布局阶段开始重新计算,而页面布局往往是一个复杂的层级系统——一个节点的重排可能引发其父节点、兄弟节点乃至整个文档树的连锁反应。这种级联重排被称为回流污染

举个例子:在页面顶部的某个元素高度增加 10px,可能迫使它下方的所有元素全部向下推移,这些元素的每一个都会经历重排和重绘。如果一个页面布局复杂、DOM 层级很深,这种操作可能在短短几毫秒内触发几百次计算,导致明显的卡顿。尤其是在低端设备上,连续重排造成的帧率下降会严重影响用户体验。

18.2.4 优化策略与最佳实践

既然重排不可避免,我们就需要尽量减少重排的频率、缩小重排的影响范围

1. 批量修改样式

不要一条一条地改,用 class 一次性切换:

// 不推荐:每条都会引起重排
element.style.width = '100px';
element.style.height = '200px';
element.style.margin = '10px';

// 推荐:一次修改
element.classList.add('new-style');

2. 使用文档碎片(DocumentFragment)

当需要插入多个新 DOM 节点时,先在内存中的文档碎片上组装完毕,最后一次插入文档:

const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
  const div = document.createElement('div');
  div.textContent = i;
  fragment.appendChild(div);
}
document.body.appendChild(fragment); // 一次性触发重排

3. 脱离文档流后做复杂操作

有两种常用方法让元素暂时“离线”:

  • 设置 display: none:元素被移出渲染树,后续修改不触发重排和重绘。修改完成后再恢复显示,触发一次重排。
  • 使用 cloneNode:克隆一个节点在内存里修改,最后替换原节点。
// 离线操作示例
const list = document.getElementById('myList');
list.style.display = 'none';
// 进行大量增删改操作...
list.style.display = 'block';

4. 读写分离,避免强制同步布局

将布局属性的读取和写入逻辑分开,不要交替进行:

// 不推荐:交替读写触发多次强制重排
elements.forEach(el => {
  const width = el.offsetWidth; // 读(强制布局)
  el.style.width = width + 10 + 'px'; // 写(重排)
});

// 推荐:先批量读,再批量写
const widths = elements.map(el => el.offsetWidth);
elements.forEach((el, i) => {
  el.style.width = widths[i] + 10 + 'px';
});

5. 利用绝对定位或固定定位

对于频繁重排的节点(如动画元素),将它设置为 position: absolutefixed,使其脱离正常文档流。这样它的重排只会影响自身,不会连累其他元素。

6. 使用 CSS 动画代替 JavaScript 动画

CSS 动画通过合成器线程执行,如果只使用 transformopacity,不会触发重排和重绘,只触发合成,性能最优。28.3 节会详细解释合成层与 GPU 加速原理。

7. 虚拟列表与懒渲染

对于超长列表,一次性渲染上千个 DOM 重排开销巨大。使用虚拟列表(只渲染可视区域附近的部分条目)可以大幅减少初始重排和后续滚动重排,具体实现在 31.5 节中介绍。

18.2.5 识别真实场景中的重排问题

浏览器开发者工具的性能面板可以录制一段操作,分析重排的次数和耗时。Chrome DevTools 的“Rendering”选项卡中还有一个Paint Flashing功能,开启后页面重绘区域会闪烁绿色,直观显示哪些区域在被不断绘制。这能帮助你快速定位不合理的重排和重绘。

一句话总结:修改视觉外观用重绘,修改几何尺寸用重排;能不用重排就不用重排,能用一次完成的就不要分多次。养成对DOM操作成本的敏感度,是每一个追求极致性能的前端工程师的必修课。