浏览器的 Reflow 和 Repaint 是什么?为什么要尽量避免它们?

30 阅读4分钟

在前端性能优化的讨论中,Reflow 和 Repaint 是两个绕不开的概念。理解它们,才能真正明白为什么 Vue 要做异步批量更新,为什么频繁操作 DOM 会让页面卡顿。

1、浏览器是怎么把页面画出来的

先简单了解浏览器渲染页面的流程:

HTML + CSS 解析
      ↓
构建 DOM 树 + CSSOM 树
      ↓
合并成 Render Tree(渲染树)
      ↓
Layout(布局/排版)← 这一步就是 Reflow
      ↓
Paint(绘制)← 这一步就是 Repaint
      ↓
Composite(合成,显示到屏幕)

页面首次加载时,这个流程走一遍。之后每次你修改 DOM 或 CSS,浏览器需要从某个步骤重新开始,这就是性能开销的来源。

2、Reflow(重排/重新布局)

定义:当页面中元素的几何信息(位置、尺寸)发生变化时,浏览器需要重新计算所有受影响元素的布局。

2.1 什么会触发 Reflow

  • 修改元素的宽高、边距、边框
  • 增加/删除 DOM 元素
  • 修改字体大小
  • 窗口 resize
  • 读取某些 DOM 属性(offsetWidthgetBoundingClientRect() 等)

2.2 为什么 Reflow 代价大

Reflow 是级联的。一个元素的尺寸变了,它的兄弟元素、父元素、子元素都可能需要重新计算位置。在复杂页面里,一次 Reflow 可能需要重新计算几百上千个元素的布局。

打个比方:你在一个书架上抽掉了一本书,旁边所有的书都会滑动重新排列——书架越大,需要重排的书越多。

3、Repaint(重绘)

定义:当元素的视觉样式改变(但不影响布局)时,浏览器需要重新绘制该元素。

3.1 什么会触发 Repaint(但不触发 Reflow)

  • 改变颜色(colorbackground-color
  • 改变透明度(opacity
  • 改变阴影(box-shadow
  • 改变轮廓(outline

3.2 Reflow 和 Repaint 的关系

Reflow 一定会触发 Repaint(布局变了,肯定要重画)。
Repaint 不一定触发 Reflow(只是颜色变了,位置没动)。

所以 Reflow 的代价 > Repaint 的代价。

4、用代码感受一下

例 1:触发多次 Reflow(性能差)

const box = document.querySelector('.box')

box.style.width = '200px'   // 触发 Reflow ①
box.style.height = '200px'  // 触发 Reflow ②
box.style.margin = '20px'   // 触发 Reflow ③

每行都触发一次 Reflow,共 3 次。

例 2:批量修改,只触发一次 Reflow(性能好)

// 方法一:一次性设置 class
box.className = 'box box--large'  // 只触发 1 次 Reflow

// 方法二:用 cssText 批量设置
box.style.cssText = 'width: 200px; height: 200px; margin: 20px'  // 只触发 1 次 Reflow

例 3:读取 DOM 属性会强制触发 Reflow

const box = document.querySelector('.box')

box.style.width = '200px'
// 以下读取操作会强制浏览器立刻完成未处理的 Reflow,才能返回准确值
const width = box.offsetWidth   // 强制 Reflow
const height = box.offsetHeight // 再次强制 Reflow

box.style.height = width + 'px' // 又触发一次 Reflow

这个"写 → 读 → 写 → 读"的交替模式是 Reflow 最常见的陷阱,叫做 Layout Thrashing(布局抖动)

优化方式:先把要读的值都读完,再统一写:

// ✅ 先读
const width = box.offsetWidth
const height = box.offsetHeight

// 再写
box.style.width = (width + 10) + 'px'
box.style.height = (height + 10) + 'px'
// 只触发 1 次 Reflow

5、哪些操作完全不触发 Reflow 和 Repaint

有些 CSS 属性由 GPU 处理,完全绕过 Reflow 和 Repaint:

  • transform(平移、缩放、旋转)
  • opacity(部分情况下)
  • filter

这也是为什么做动画时,推荐用 transform: translateX() 代替直接修改 left 值——前者不触发 Reflow,后者触发。

/* ❌ 性能差:触发 Reflow */
.box {
  transition: left 0.3s;
}

/* ✅ 性能好:GPU 处理,不触发 Reflow */
.box {
  transition: transform 0.3s;
}

6、回到 Vue:为什么批量更新这么重要

现在再看 Vue 的异步批处理,逻辑就很清晰了:

// Vue 同步更新(假设):改 3 次数据 → 3 次 Reflow
this.width = 200
this.height = 200
this.visible = true

// Vue 异步批处理(实际):改 3 次数据 → 合并 → 1 次 Reflow

Vue 的异步更新本质上就是在帮你避免 Layout Thrashing,把多次 DOM 修改合并成一次,只触发一次 Reflow + Repaint。

7、总结

ReflowRepaint
触发条件几何信息变化(尺寸、位置)视觉样式变化(颜色、阴影)
代价重新计算整个页面布局,代价大只重绘元素外观,代价相对小
是否连带触发另一个Reflow 必然触发 RepaintRepaint 不触发 Reflow
如何减少批量修改 DOM、使用 transform 做动画使用 transform/opacity 代替触发 Repaint 的属性

记住一句话:Reflow 和 Repaint 本身不可避免,但可以通过批量操作来减少触发次数——这也是 Vue 异步更新 DOM 的根本动机。