面试这么答之回流与重绘

92 阅读5分钟

一文搞懂回流与重绘:原理+优化方案+面试答法

一、浏览器是如何渲染页面的?

要了解回流与重绘,首先要了解浏览器的渲染页面的过程:

  1. 解析HTML -> 构建DOM树
  2. 解析CSS -> 构建CSSOM树
  3. 合并两者 -> 生成Render Tree(渲染树)
  4. 计算每个节点的几何位置(布局阶段)
  5. 绘制到屏幕(绘制阶段)

当我们用JS改变页面(如修改样式、插入元素),浏览器就必须重新执行其中的一部分步骤。这就产生了两种性能代价不同的操作:回流重绘

二、回流与重绘是什么

1. 回流(Reflow)

回流又叫“重排”,是指当元素的几何尺寸或布局发生变化时,浏览器需要重新计算布局。

常见触发场景:

  • 改变元素的宽高、内外边距、边框;
  • 显示或隐藏元素(display:none -> block);
  • 修改 DOM 结构(增删节点)
  • 改变窗口大小;
  • 读取某些布局属性(如offsetHeight,clientTop等)

回流代价较高,因为浏览器可能需要重新计算整个页面的布局,尤其当频繁触发时,会导致页面卡顿。

2. 重绘(Repaint)

重绘是指当元素外观(如颜色,背景)发生变化但布局未变时,浏览器只需重新绘制视觉样式。

常见触发场景:

  • 修改字体颜色,背景颜色;
  • 改变阴影、透明度等非几何属性;

重绘不需要重新计算布局,因此代价比回流小得多。

两者的关系:回流一定会引起重绘,但重绘不一定会引起回流

回流会导致元素布局变化,自然需要重新绘制;而仅改变颜色等样式不会影响布局,因此只触发重绘。

//举个例子
//触发回流+重绘
div.style.height='200px'
//仅触发重绘
div.style.backgroundColor='red'

三、框架层面:回流与重绘依然存在

那么现在有一个问题:用VueReact框架,这些问题会如何变化?

框架通过虚拟 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;
}

相比之下,topleftheight的动画则会频繁触发布局计算。

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,脱离文档流;
  • 动画尽量用 transformopacity,利用 GPU 合成;
  • 避免复杂选择器和频繁调用 getComputedStyle