一文搞懂回流与重绘:原理+优化方案+面试答法
一、浏览器是如何渲染页面的?
要了解回流与重绘,首先要了解浏览器的渲染页面的过程:
- 解析HTML -> 构建DOM树
- 解析CSS -> 构建CSSOM树
- 合并两者 -> 生成Render Tree(渲染树)
- 计算每个节点的几何位置(布局阶段)
- 绘制到屏幕(绘制阶段)
当我们用JS改变页面(如修改样式、插入元素),浏览器就必须重新执行其中的一部分步骤。这就产生了两种性能代价不同的操作:回流 和 重绘
二、回流与重绘是什么
1. 回流(Reflow)
回流又叫“重排”,是指当元素的几何尺寸或布局发生变化时,浏览器需要重新计算布局。
常见触发场景:
- 改变元素的宽高、内外边距、边框;
- 显示或隐藏元素(
display:none->block); - 修改 DOM 结构(增删节点)
- 改变窗口大小;
- 读取某些布局属性(如
offsetHeight,clientTop等)
回流代价较高,因为浏览器可能需要重新计算整个页面的布局,尤其当频繁触发时,会导致页面卡顿。
2. 重绘(Repaint)
重绘是指当元素外观(如颜色,背景)发生变化但布局未变时,浏览器只需重新绘制视觉样式。
常见触发场景:
- 修改字体颜色,背景颜色;
- 改变阴影、透明度等非几何属性;
重绘不需要重新计算布局,因此代价比回流小得多。
两者的关系:回流一定会引起重绘,但重绘不一定会引起回流
回流会导致元素布局变化,自然需要重新绘制;而仅改变颜色等样式不会影响布局,因此只触发重绘。
//举个例子
//触发回流+重绘
div.style.height='200px'
//仅触发重绘
div.style.backgroundColor='red'
三、框架层面:回流与重绘依然存在
那么现在有一个问题:用Vue或React框架,这些问题会如何变化?
框架通过虚拟 DOM(Virtual DOM) 或 Diff 算法减少不必要的真实 DOM 操作,但最终浏览器仍然需要:
- React 对真实 DOM 进行 patch;
- Vue 更新模板对应节点;
- 浏览器执行回流与重绘。
框架减少了触发次数,但无法消除回流重绘机制。
四、如何优化回流与重绘
1.合并多次DOM操作
//多回流写法:每修改一次样式,浏览器都有可能重新计算布局
div.style.width = '100px'
div.style.height = '200px'
div.style.margin = '10px'
//优化写法:使用class或cssText一次性修改,减少回流次数
div.style.cssText = 'width:100px;height:200px;margin:10px;'
div.classList.add('active')
2.读写分离,避免强制同步回流
浏览器会对 DOM 操作进行“批量优化”,但如果在修改后立刻读取布局信息,会强制回流。
//错误写法:立即读取,导致强制回流
div.style.width='100px'
console.log(div.offsetWidth)
//优化写法:利用requestAnimationFrame让浏览器在下一个绘制周期中执行,避免阻塞
requestAnimationFrame(()=>{
console.log(div.offsetWidth)
})
3.批量操作离线DOM
在修改大量节点时,可先脱离 DOM 树操作,再统一插入。
// 创建一个 DocumentFragment,它是一个轻量级的 DOM 容器
// 不属于页面的主 DOM 树,所以对它的操作不会触发回流或重绘
const fragment = document.createDocumentFragment()
// 使用 for 循环创建 1000 个 <li> 元素
for (let i = 0; i < 1000; i++) {
// 创建一个新的 <li> 元素
const li = document.createElement('li')
// 将 <li> 临时添加到 fragment 中
// 由于 fragment 不在真实 DOM 树中,操作不会触发回流,直到 fragment 被插入到 DOM 时才触发一次回流
fragment.appendChild(li)
}
// 循环结束后,将 fragment 一次性插入到页面中的 <ul> 元素里
// 这时才会触发一次回流和重绘,而不是 1000 次
ul.appendChild(fragment)
DocumentFragment 不在 DOM 树中,操作不会触发回流,最后一次性插入 DOM。
4.使用position:absolute/fixed
脱离文档流的元素,其变化不会影响其他元素的布局,从而减少回流范围。
.modal{
position:fixed;
top:0;
left:0;
}
5.动画尽量使用transform与opacity
这两个属性只会出现在合成阶段,不会引发布局或绘制,性能极佳。
.box {
transition: transform 0.3s ease, opacity 0.3s ease;
}
相比之下,top、left、height的动画则会频繁触发布局计算。
6.减少频繁的样式计算
- 合并CSS文件,减少样式层级;
- 避免使用复杂选择器(如:
div > p span.class); - 避免在JS中频繁调用
getComputedStyle。
五、面试延伸
Q1:transform 和 opacity 为什么性能好?
它们在合成层中由 GPU 处理,不会引发布局计算(reflow)或重绘(repaint),只触发合成(composite)。
Q2:如何快速判断代码中可能引发性能问题?
可以打开 Chrome Performance 面板,观察是否频繁出现 Recalculate Style 或 Layout 事件。
六、推荐面试回答模板:
浏览器渲染页面时,会根据 DOM 和 CSSOM 构建渲染树。当元素的布局或几何属性发生变化时,会触发 回流(Reflow) ,当只改变视觉属性时,会触发 重绘(Repaint) 。回流比重绘开销大,因为需要重新计算布局,并可能影响子元素和整个页面。
优化原则是减少回流重绘的次数和影响范围:
- 合并多次 DOM 操作,尽量一次性修改样式或使用 class 切换;
- 读写分离,避免修改后立即读取布局信息,使用
requestAnimationFrame批量处理; - 批量操作离线 DOM(如
DocumentFragment); - 对频繁变化的元素使用
position: absolute / fixed,脱离文档流; - 动画尽量用
transform和opacity,利用 GPU 合成; - 避免复杂选择器和频繁调用
getComputedStyle。