回流(Reflow)和重绘(Repaint) 是浏览器渲染网页过程中两个关键的性能相关概念,理解它们有助于优化前端性能。
一、基本概念
1. 回流(Reflow / Layout)
-
定义:当 DOM 元素的几何属性(如宽高、位置、边距等)发生变化时,浏览器需要重新计算元素的布局(即“布局树”或“盒模型”),这个过程称为回流。
-
触发条件(常见):
- 改变元素的
width、height、padding、margin、border等尺寸属性 - 添加/删除可见的 DOM 元素
- 改变字体大小(
font-size) - 用户行为(如窗口缩放、滚动)
- 调用某些 JavaScript 属性(如
offsetWidth、clientHeight、getComputedStyle()等)会强制触发回流(称为“强制同步布局”)
- 改变元素的
回流会影响后续所有元素的布局,甚至可能影响整个页面,因此开销较大。
2. 重绘(Repaint / Redraw)
-
定义:当元素的外观(如颜色、背景、边框样式等)发生变化,但不影响布局时,浏览器只需重新绘制该元素的视觉表现,这个过程称为重绘。
-
触发条件(常见):
- 改变
color、background-color、visibility(注意:visibility: hidden不触发回流,但会重绘) - 修改
outline、box-shadow等不影响布局的样式
- 改变
重绘不一定触发回流,但回流一定会触发重绘(因为布局变了,画面自然要更新)。
二、性能影响
- 回流 > 重绘 > 普通渲染(性能开销依次降低)
- 频繁的回流/重绘会导致页面卡顿、掉帧,影响用户体验。
三、优化策略
✅ 减少回流和重绘的方法:
-
批量修改 DOM 或样式
js // ❌ 错误:多次触发回流 el.style.width = '100px'; el.style.height = '100px'; el.style.color = 'red'; // ✅ 正确:合并为一次操作 el.style.cssText = 'width: 100px; height: 100px; color: red;'; // 或使用 class 切换 el.className = 'new-style'; -
避免频繁读取会触发回流的属性
js // ❌ 每次读取 offsetWidth 都可能触发回流 for (let i = 0; i < 100; i++) { el.style.left = el.offsetWidth + 10 + 'px'; } // ✅ 先缓存值 const width = el.offsetWidth; for (let i = 0; i < 100; i++) { el.style.left = width + 10 + 'px'; } -
使用文档片段(DocumentFragment)操作大量 DOM
js const fragment = document.createDocumentFragment(); for (let i = 0; i < 1000; i++) { const li = document.createElement('li'); li.textContent = i; fragment.appendChild(li); } list.appendChild(fragment); // 只触发一次回流 -
将频繁变化的元素脱离文档流
- 使用
position: absolute或fixed,使其不影响其他元素布局。 - 动画元素尽量使用
transform和opacity(这些属性由合成器处理,不触发回流/重绘)。
- 使用
-
使用 CSS 动画代替 JS 动画
- 优先使用
transform、opacity实现动画,它们可以被 GPU 加速,且不会触发回流。
- 优先使用
-
避免使用 table 布局
- 表格中一个单元格变化可能导致整个表格重新计算(回流代价高)。
四、现代浏览器的优化
- 浏览器会自动合并多个回流/重绘操作(如在同一个事件循环中),延迟执行以提升性能。
- 但强制同步布局(如读取
offsetWidth后立即写样式)会打断这种优化,导致“布局抖动”(Layout Thrashing)。
总结
| 操作 | 触发 |
|---|---|
width/height/padding/margin 改变 | ✅ 回流 + 重绘 |
| 添加/删除可见 DOM 元素 | ✅ 回流 + 重绘 |
font-size 改变 | ✅ 回流 + 重绘 |
color/background-color 改变 | ❌ 回流,✅ 重绘 |
visibility: hidden | ❌ 回流,✅ 重绘 |
display: none | ✅ 回流 + 重绘 |
transform/opacity 改变 | ❌ 回流,❌ 重绘(由合成器处理) |
读取 offsetWidth、clientHeight 等 | ⚠️ 可能强制同步回流 |
💡 最佳实践:尽量减少 DOM 操作,使用 CSS 类控制样式,动画使用
transform和opacity,避免强制同步布局。
其他QS
1. 为什么读取 offsetWidth 会触发回流?
✅ 深入原理:
- 浏览器为了返回准确的布局值(如
offsetWidth),必须立即执行所有待处理的回流任务,这称为 强制同步布局(Forced Synchronous Layout) 。 - 如果在循环中反复读写,会导致 布局抖动(Layout Thrashing) ,性能极差。
js
// ❌ 危险代码:每次循环都强制回流
for (let i = 0; i < 100; i++) {
el.style.width = el.offsetWidth + 10 + 'px'; // 读 + 写 → 强制回流
}
💡 高分点:提到“布局抖动”术语,并给出解决方案(先缓存
offsetWidth)。
2. 现代浏览器如何优化回流/重绘?
✅ 加分回答:
- 浏览器会将多个 DOM 修改合并成一次回流(队列化,延迟执行)。
- 但强制同步操作(如读取布局属性)会打断优化。
- Chrome DevTools 的 Performance 面板 可以录制并分析回流/重绘耗时。
💡 高分点:提到 DevTools 工具,体现工程实践能力。
3. visibility: hidden 和 display: none的区别?
| 特性 | visibility: hidden | display: none |
|---|---|---|
| 是否占据空间 | ✅ 占据原有布局空间(“隐身但占位”) | ❌ 完全从文档流中移除(不占位) |
| 是否触发回流(Reflow) | ❌ 不触发回流 | ✅ 触发回流(因为布局改变) |
| 是否触发重绘(Repaint) | ✅ 触发重绘(元素不可见但需清除像素) | ✅ 触发重绘(连同子元素一起消失) |
| 子元素是否可见 | ❌ 子元素默认也隐藏(但可通过 visibility: visible 覆盖) | ❌ 子元素完全不可见且无法显示 |
| 是否响应事件 | ❌ 不响应鼠标/键盘事件 | ❌ 不响应事件 |
| 是否影响可访问性(如屏幕阅读器) | ⚠️ 通常仍会被读出(取决于浏览器) | ✅ 完全忽略,不会被读出 |
4. opacity
opacity(注意正确拼写是 opacity,不是 "opcity")在现代浏览器中具有一项重要的性能优化机制:它可以通过 GPU 加速进行合成(Compositing),而不会触发回流(Reflow)或重绘(Repaint) 。这是前端性能优化中的关键知识点。
✅ 一、opacity 的特殊优化原理
1. 不触发回流 & 重绘
-
修改
opacity不会改变元素的几何属性(如宽高、位置),因此:- ❌ 不触发 回流(Reflow)
- ❌ 不触发 重绘(Repaint)
2. 触发“合成”(Compositing)
- 当
opacity值 小于 1(如opacity: 0.9)时,浏览器会将该元素提升为一个独立的合成层(Composited Layer) 。 - 后续的
opacity动画由 GPU 直接处理,通过调整图层的透明度来实现视觉变化,无需重新绘制像素或计算布局。 - 这个过程称为 “合成”(Compositing) ,发生在渲染流程的最后一步。
📌 渲染流程简化:
Style → Layout(回流) → Paint(重绘) → Composite(合成)
opacity 只影响 Composite 阶段。
二、为什么 opacity 能被 GPU 加速?
-
浏览器(如 Chrome)会为满足特定条件的元素创建 “合成层”(Compositing Layer) ,例如:
- 使用
transform - 使用
opacity < 1 - 使用
will-change: opacity - 有
video、canvas、iframe等
- 使用
-
一旦成为合成层,该元素会被上传到 GPU 纹理(Texture) ,后续动画由 GPU 直接操作纹理的透明度,效率极高。
三、注意事项
opacity: 1不会创建合成层
只有opacity < 1时才会触发合成优化。如果初始就是1,动画开始时才变小,可能首帧仍有开销。- 过度使用合成层会导致“层爆炸”(Layer Explosion)
每个合成层都占用 GPU 内存,过多会导致内存压力。可通过will-change或transform: translateZ(0)显式提升,但要谨慎。 - 子元素继承透明度
opacity会影响整个元素及其所有子元素的透明度,无法单独控制子元素不透明(此时可考虑rgba背景色)。