前端八股性能优化(2)---回流(重排)和重绘

目录


一、核心区别速览

对比维度 回流(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:哪些操作会触发回流?

答:

  1. 宽高、边距、边框、定位改变

  2. 增删 DOM 节点

  3. 浏览器窗口 resize

  4. 字体大小变化

  5. 读取 offsetWidth、getComputedStyle 等布局属性

Q3:如何减少回流和重绘?

答:

  1. 批量修改样式(使用 class 或 cssText)

  2. 先读取布局属性,再修改(避免强制回流)

  3. 使用 transform 代替 left/top

  4. 使用 opacity 代替 visibility/display

  5. 将动画元素设为绝对定位,脱离文档流

  6. 使用 requestAnimationFrame 优化动画

  7. 使用 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:哪些属性会触发合成?

答: transformopacityfilter 等属性只会触发合成,不会触发回流和重绘,是性能最好的动画属性。

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操作
相关推荐
程序员buddha4 小时前
深入理解ES6 Promise
前端·ecmascript·es6
吴声子夜歌4 小时前
ES6——Module详解
前端·ecmascript·es6
剪刀石头布啊5 小时前
原生form发起表单干了啥
前端
剪刀石头布啊5 小时前
表单校验场景,如何实现页面滚动到报错位置
前端
gyx_这个杀手不太冷静5 小时前
大人工智能时代下前端界面全新开发模式的思考(二)
前端·架构·ai编程
GreenTea5 小时前
AI Agent 评测的下半场:从方法论到落地实践
前端·人工智能·后端
吴声子夜歌5 小时前
Vue3——Vue实例与数据绑定
前端·javascript·vue.js
我是若尘6 小时前
Harness Engineering:2026 年 AI 编程的核心战场
前端·后端·程序员
weixin199701080166 小时前
《快手商品详情页前端性能优化实战》
前端·性能优化