目录
- 一、核心区别速览
- 二、回流(Reflow/重排)详解
- [2.1 定义](#2.1 定义)
- [2.2 触发回流的常见操作](#2.2 触发回流的常见操作)
- [2.3 浏览器的渲染队列优化(重要!)](#2.3 浏览器的渲染队列优化(重要!))
- ①元素尺寸/位置属性
- ②客户端区域尺寸/位置
- ③滚动区域属性
- ④方法类
- [2.4 回流的影响范围](#2.4 回流的影响范围)
- 三、重绘(Repaint)详解
- [3.1 定义](#3.1 定义)
- [3.2 触发重绘的常见样式](#3.2 触发重绘的常见样式)
- [3.3 重绘 vs 回流关系](#3.3 重绘 vs 回流关系)
- 四、性能优化建议
- [4.1 避免强制回流](#4.1 避免强制回流)
- [4.2 批量修改样式](#4.2 批量修改样式)
- [4.3 脱离文档流](#4.3 脱离文档流)
- [4.4 使用 transform 代替位置属性](#4.4 使用 transform 代替位置属性)
- [4.5 使用 opacity 代替 visibility/display](#4.5 使用 opacity 代替 visibility/display)
- [4.6 使用 requestAnimationFrame](#4.6 使用 requestAnimationFrame)
- 五、完整优化示例
- 六、面试高频问题
- Q1:什么是回流和重绘?有什么区别?
- Q2:哪些操作会触发回流?
- Q3:如何减少回流和重绘?
- [Q4:为什么读取 offsetWidth 会导致回流?](#Q4:为什么读取 offsetWidth 会导致回流?)
- [Q5:v-if 和 v-show 分别会触发回流/重绘吗?](#Q5:v-if 和 v-show 分别会触发回流/重绘吗?)
- Q6:什么是合成?为什么合成性能最好?
- Q7:哪些属性会触发合成?
- Q8:如何强制创建独立图层?
- Q:合成和重绘的区别?
- 七、性能开销对比
一、核心区别速览
| 对比维度 | 回流(Reflow/重排) | 重绘(Repaint) |
|---|---|---|
| 触发条件 | 几何属性变化(宽高、位置、布局) | 外观属性变化(颜色、背景、阴影) |
| 影响范围 | 可能影响整个页面布局 | 只影响当前元素外观 |
| 性能开销 | 非常大(严重) | 相对较小 |
| 是否包含另一个 | ✅ 回流一定触发重绘 | ❌ 重绘不一定触发回流 |
一句话总结:
-
回流:改变布局(比如拆墙、挪家具)→ 开销大
-
重绘:只改外观(比如刷漆、换壁纸)→ 开销小
二、回流(Reflow/重排)详解
2.1 定义
当页面中元素的尺寸、布局、隐藏显示发生改变时,浏览器重新计算几何属性并重新排列元素的过程,叫回流。
2.2 触发回流的常见操作
| 分类 | 具体操作 |
|---|---|
| 几何属性变化 | width、height、padding、margin、border 改变 |
| 定位变化 | top、left、right、bottom、position 改变 |
| DOM 操作 | 增删 DOM 节点、改变内容 |
| 字体变化 | font-size、font-family 改变 |
| 窗口变化 | resize 浏览器窗口 |
| 读取布局属性 | offsetWidth、offsetHeight、clientWidth、getComputedStyle 等 |
| 激活伪类 | hover 等导致样式变化 |
| CSS 属性 | display: none/block、float、clear |
2.3 浏览器的渲染队列优化(重要!)
javascript
// 浏览器很"懒",会批量处理样式修改
div.style.width = '100px' // 放进队列
div.style.height = '200px' // 放进队列
div.style.margin = '10px' // 放进队列
// 等 JS 执行完,一次性回流,性能高
// ❌ 但是!一旦读取布局属性,浏览器就"慌了"
div.style.width = '100px'
console.log(div.offsetWidth) // 立即强制回流!要给你最新值
div.style.height = '200px'
console.log(div.clientHeight) // 又强制回流!
为什么读取布局属性会强制回流?
浏览器心想:
"我前面还有一堆样式修改没算呢,如果你现在要拿宽度,我必须给你最新正确的值,不能给你旧的、错的!"
所以它被迫立刻清空队列,强制回流一次,把最新布局算出来,再返回给你。
常见的强制回流操作:
javascript
// 读取以下属性/方法都会强制回流
element.offsetTop / offsetLeft / offsetWidth / offsetHeight
element.clientTop / clientLeft / clientWidth / clientHeight
element.scrollTop / scrollLeft / scrollWidth / scrollHeight
element.getBoundingClientRect()
window.getComputedStyle(element)
element.scrollIntoView()
①元素尺寸/位置属性
| 属性 | 说明 | 返回值 |
|---|---|---|
offsetTop |
元素相对于 offsetParent 上边缘的距离 | 数值(px) |
offsetLeft |
元素相对于 offsetParent 左边缘的距离 | 数值(px) |
offsetWidth |
元素布局宽度(含宽+内边距+边框) | 数值(px) |
offsetHeight |
元素布局高度(含高+内边距+边框) | 数值(px) |
②客户端区域尺寸/位置
| 属性 | 说明 | 返回值 |
|---|---|---|
clientTop |
元素上边框宽度 | 数值(px) |
clientLeft |
元素左边框宽度 | 数值(px) |
clientWidth |
元素内部宽度(宽+内边距,不含边框) | 数值(px) |
clientHeight |
元素内部高度(高+内边距,不含边框) | 数值(px) |
③滚动区域属性
| 属性 | 说明 | 返回值 |
|---|---|---|
scrollTop |
滚动条滚动的垂直距离 | 数值(px) |
scrollLeft |
滚动条滚动的水平距离 | 数值(px) |
scrollWidth |
元素内容的实际宽度 | 数值(px) |
scrollHeight |
元素内容的实际高度 | 数值(px) |
④方法类
| 方法 | 说明 | 返回值 |
|---|---|---|
getBoundingClientRect() |
获取元素相对于视口的位置和尺寸 | DOMRect 对象 |
getComputedStyle() |
获取元素最终计算后的样式 | CSSStyleDeclaration 对象 |
scrollIntoView() |
将元素滚动到可视区域 | void(但会触发回流) |
2.4 回流的影响范围
改变一个元素,可能影响:
├── 父元素(重新计算子元素位置)
├── 兄弟元素(位置变化)
├── 子元素(大小变化)
└── 整个页面(如 resize)
三、重绘(Repaint)详解
3.1 定义
元素外观、颜色、样式发生改变,但不影响它在页面中的位置和大小时,浏览器只重新绘制这个元素的像素,这个过程就叫重绘。
3.2 触发重绘的常见样式
| 分类 | 具体属性 |
|---|---|
| 颜色相关 | color、background-color、border-color |
| 透明度 | opacity |
| 可见性 | visibility |
| 阴影 | box-shadow、text-shadow |
| 背景 | background-image、background-position |
| 轮廓 | outline |
| 文字装饰 | text-decoration |
3.3 重绘 vs 回流关系
┌─────────────────────────────────────────────────────────┐
│ │
│ 回流(Reflow) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 几何属性变化 → 重新计算布局 → 重新绘制 │ │
│ │ │ │
│ │ 一定会触发 ↓ │ │
│ └─────────────────────────────────────────────────┘ │
│ ↓ │
│ 重绘(Repaint) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 外观属性变化 → 重新绘制像素 │ │
│ │ 不触发回流 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 结论:回流一定会触发重绘,但重绘不一定触发回流 │
│ │
└─────────────────────────────────────────────────────────┘
四、性能优化建议
4.1 避免强制回流
javascript
// ❌ 错误:多次读取布局属性
for (let i = 0; i < 1000; i++) {
console.log(div.offsetWidth) // 每次都可能触发回流
}
// ✅ 正确:先读取,后修改
const width = div.offsetWidth // 读取一次
for (let i = 0; i < 1000; i++) {
// 使用缓存的值
}
4.2 批量修改样式
javascript
// ❌ 错误:多次修改,多次回流
div.style.width = '100px'
div.style.height = '100px'
div.style.margin = '10px'
// ✅ 正确:使用 class 批量修改
div.classList.add('new-style')
// ✅ 正确:使用 cssText
div.style.cssText = 'width: 100px; height: 100px; margin: 10px;'
4.3 脱离文档流
javascript
// 对复杂动画使用绝对定位,脱离文档流
// 这样动画过程中只影响自身,不会导致父元素和兄弟元素回流
.element {
position: absolute; /* 或 fixed */
/* 动画只影响自己 */
}
4.4 使用 transform 代替位置属性
javascript
/* ❌ 会触发回流 */
.element {
position: absolute;
left: 10px;
top: 10px;
}
/* ✅ 只触发重绘/合成,性能更好 */
.element {
transform: translate(10px, 10px);
}
4.5 使用 opacity 代替 visibility/display
javascript
/* ❌ visibility: hidden 触发重绘 */
/* ❌ display: none 触发回流 */
/* ✅ opacity: 0 只触发合成,性能最好 */
.element {
opacity: 0;
pointer-events: none; /* 禁用点击 */
}
4.6 使用 requestAnimationFrame
javascript
// ❌ 错误:频繁修改样式
function animate() {
div.style.left = div.offsetLeft + 1 + 'px' // 强制回流
}
setInterval(animate, 16)
// ✅ 正确:使用 requestAnimationFrame
function animate() {
div.style.transform = `translateX(${x}px)`
x++
requestAnimationFrame(animate)
}
五、完整优化示例
javascript
// 场景:批量更新100个元素
const items = document.querySelectorAll('.item')
// ❌ 错误写法:每个元素都触发回流
items.forEach(item => {
item.style.width = '100px'
item.style.height = '100px'
item.style.margin = '10px'
})
// ✅ 优化1:使用 class
items.forEach(item => {
item.classList.add('new-size')
})
// ✅ 优化2:使用 cssText
items.forEach(item => {
item.style.cssText = 'width: 100px; height: 100px; margin: 10px;'
})
// ✅ 优化3:使用 DocumentFragment(添加多个DOM)
const fragment = document.createDocumentFragment()
for (let i = 0; i < 100; i++) {
const div = document.createElement('div')
div.textContent = i
fragment.appendChild(div)
}
container.appendChild(fragment) // 只触发一次回流
// ✅ 优化4:使用 display: none 批量操作
container.style.display = 'none' // 脱离文档流
// 批量修改...
for (let i = 0; i < 100; i++) {
const item = items[i]
item.style.width = '100px'
item.style.height = '100px'
}
container.style.display = 'block' // 重新显示,只触发一次回流
六、面试高频问题
Q1:什么是回流和重绘?有什么区别?
答:
回流(重排):当元素的几何属性(宽高、位置、布局)改变时,浏览器重新计算布局的过程。开销大。
重绘:当元素的外观属性(颜色、背景、阴影)改变时,浏览器重新绘制像素的过程。开销相对小。
区别:回流一定触发重绘,但重绘不一定触发回流。
Q2:哪些操作会触发回流?
答:
宽高、边距、边框、定位改变
增删 DOM 节点
浏览器窗口 resize
字体大小变化
读取 offsetWidth、getComputedStyle 等布局属性
Q3:如何减少回流和重绘?
答:
批量修改样式(使用 class 或 cssText)
先读取布局属性,再修改(避免强制回流)
使用 transform 代替 left/top
使用 opacity 代替 visibility/display
将动画元素设为绝对定位,脱离文档流
使用 requestAnimationFrame 优化动画
使用 DocumentFragment 批量添加 DOM
- 【Vue 3 会自动合并 DOM 操作 ,通过异步批量更新机制,将同一个事件循环内的多次数据修改合并成一次 DOM 更新,从而减少重排重绘。】
Q4:为什么读取 offsetWidth 会导致回流?
答:
浏览器有渲染队列优化,会把样式修改暂存起来,等 JS 执行完一次性处理。但当读取 offsetWidth 等布局属性时,浏览器必须立即返回正确的值,所以会强制清空队列,立即执行回流计算最新布局。
Q5:v-if 和 v-show 分别会触发回流/重绘吗?
答:
v-if:添加/删除 DOM 节点,一定会触发回流和重绘,且每次切换都要重建 DOM
v-show :切换
display: none,因为元素从文档流中消失/出现,也会触发回流和重绘为什么还说 v-show 性能更好?
虽然两者都会触发回流,但 v-show 的切换只是改 CSS 属性,回流开销相对小;而 v-if 每次都要创建/销毁 DOM,涉及 DOM 操作 + 回流,开销大得多。
所以:
频繁切换 → 用 v-show(只改 display)
很少切换 → 用 v-if(节省初始渲染开销)
Q6:什么是合成?为什么合成性能最好?
答: 合成是浏览器将多个图层合并成最终屏幕显示的过程。合成操作在 GPU 上执行,利用硬件加速,不涉及布局计算和像素绘制,所以性能最好。
Q7:哪些属性会触发合成?
答: transform、opacity、filter 等属性只会触发合成,不会触发回流和重绘,是性能最好的动画属性。
Q8:如何强制创建独立图层?
答:
css
.box {
will-change: transform; /* 提前声明 */
transform: translateZ(0); /* 3D变换 */
backface-visibility: hidden; /* 强制GPU加速 */
}
Q:合成和重绘的区别?
答: 重绘是在 CPU 上重新绘制像素,合成是在 GPU 上合并图层。合成不涉及像素绘制,只做图层合并,速度远快于重绘。
七、性能开销对比
性能开销(从小到大):
合成(Composite) < 重绘(Repaint) < 回流(Reflow)
触发合成:transform、opacity(GPU加速)
触发重绘:color、background、box-shadow
触发回流:width、height、margin、DOM操作