在前端性能优化的讨论中,Reflow 和 Repaint 是两个绕不开的概念。理解它们,才能真正明白为什么 Vue 要做异步批量更新,为什么频繁操作 DOM 会让页面卡顿。
1、浏览器是怎么把页面画出来的
先简单了解浏览器渲染页面的流程:
css
HTML + CSS 解析
↓
构建 DOM 树 + CSSOM 树
↓
合并成 Render Tree(渲染树)
↓
Layout(布局/排版)← 这一步就是 Reflow
↓
Paint(绘制)← 这一步就是 Repaint
↓
Composite(合成,显示到屏幕)
页面首次加载时,这个流程走一遍。之后每次你修改 DOM 或 CSS,浏览器需要从某个步骤重新开始,这就是性能开销的来源。
2、Reflow(重排/重新布局)
定义 :当页面中元素的几何信息(位置、尺寸)发生变化时,浏览器需要重新计算所有受影响元素的布局。
2.1 什么会触发 Reflow
- 修改元素的宽高、边距、边框
- 增加/删除 DOM 元素
- 修改字体大小
- 窗口 resize
- 读取某些 DOM 属性(
offsetWidth、getBoundingClientRect()等)
2.2 为什么 Reflow 代价大
Reflow 是级联的。一个元素的尺寸变了,它的兄弟元素、父元素、子元素都可能需要重新计算位置。在复杂页面里,一次 Reflow 可能需要重新计算几百上千个元素的布局。
打个比方:你在一个书架上抽掉了一本书,旁边所有的书都会滑动重新排列------书架越大,需要重排的书越多。
3、Repaint(重绘)
定义 :当元素的视觉样式改变(但不影响布局)时,浏览器需要重新绘制该元素。
3.1 什么会触发 Repaint(但不触发 Reflow)
- 改变颜色(
color、background-color) - 改变透明度(
opacity) - 改变阴影(
box-shadow) - 改变轮廓(
outline)
3.2 Reflow 和 Repaint 的关系
Reflow 一定会触发 Repaint (布局变了,肯定要重画)。
Repaint 不一定触发 Reflow(只是颜色变了,位置没动)。
所以 Reflow 的代价 > Repaint 的代价。
4、用代码感受一下
例 1:触发多次 Reflow(性能差)
ini
const box = document.querySelector('.box')
box.style.width = '200px' // 触发 Reflow ①
box.style.height = '200px' // 触发 Reflow ②
box.style.margin = '20px' // 触发 Reflow ③
每行都触发一次 Reflow,共 3 次。
例 2:批量修改,只触发一次 Reflow(性能好)
rust
// 方法一:一次性设置 class
box.className = 'box box--large' // 只触发 1 次 Reflow
// 方法二:用 cssText 批量设置
box.style.cssText = 'width: 200px; height: 200px; margin: 20px' // 只触发 1 次 Reflow
例 3:读取 DOM 属性会强制触发 Reflow
arduino
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(布局抖动) 。
优化方式:先把要读的值都读完,再统一写:
arduino
// ✅ 先读
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,后者触发。
css
/* ❌ 性能差:触发 Reflow */
.box {
transition: left 0.3s;
}
/* ✅ 性能好:GPU 处理,不触发 Reflow */
.box {
transition: transform 0.3s;
}
6、回到 Vue:为什么批量更新这么重要
现在再看 Vue 的异步批处理,逻辑就很清晰了:
kotlin
// Vue 同步更新(假设):改 3 次数据 → 3 次 Reflow
this.width = 200
this.height = 200
this.visible = true
// Vue 异步批处理(实际):改 3 次数据 → 合并 → 1 次 Reflow
Vue 的异步更新本质上就是在帮你避免 Layout Thrashing,把多次 DOM 修改合并成一次,只触发一次 Reflow + Repaint。
7、总结
| Reflow | Repaint | |
|---|---|---|
| 触发条件 | 几何信息变化(尺寸、位置) | 视觉样式变化(颜色、阴影) |
| 代价 | 重新计算整个页面布局,代价大 | 只重绘元素外观,代价相对小 |
| 是否连带触发另一个 | Reflow 必然触发 Repaint | Repaint 不触发 Reflow |
| 如何减少 | 批量修改 DOM、使用 transform 做动画 | 使用 transform/opacity 代替触发 Repaint 的属性 |
记住一句话:Reflow 和 Repaint 本身不可避免,但可以通过批量操作来减少触发次数------这也是 Vue 异步更新 DOM 的根本动机。