浏览器 什么是重绘,重排

90 阅读3分钟

🟩前端面试题:重绘与重排

🟩先理解浏览器渲染流程

要搞懂重绘重排,先要知道浏览器是怎么把代码变成画面的:

HTML → DOM树
CSS  → CSSOM树
         ↓
      合并成 Render Tree(渲染树)
         ↓
      Layout(布局)→ 计算每个元素的位置和大小
         ↓
      Paint(绘制)→ 把像素画到屏幕上
         ↓
      Composite(合成)→ 多层合并展示

重排和重绘,就是这个流程中某些步骤被重新触发了。


🟩重排(Reflow)

又叫回流,触发时从 Layout 阶段重新开始,代价最大。

触发原因:任何影响元素---几何信息---的操作

// 以下操作都会触发重排
el.style.width = '200px'        // 改宽高
el.style.margin = '10px'        // 改外边距
el.style.display = 'none'       // 改显示方式
el.style.position = 'absolute'  // 改定位
document.body.appendChild(el)   // 增删 DOM
​
// 读取以下属性也会触发重排(强制同步布局)
el.offsetWidth
el.offsetHeight
el.clientTop
el.getBoundingClientRect()
window.getComputedStyle(el)

重排一定会引起重绘,并且会影响父元素和周围元素,代价非常高。


🟩重绘(Repaint)

触发时从 Paint 阶段重新开始,不影响布局,代价比重排小。

触发原因:影响外观但不影响布局的操作

// 以下操作只触发重绘,不触发重排
el.style.color = 'red'              // 改颜色
el.style.backgroundColor = 'blue'  // 改背景色
el.style.borderColor = 'green'     // 改边框颜色
el.style.visibility = 'hidden'     // 改可见性(注意不是 display)
el.style.boxShadow = '...'         // 改阴影

🟩两者关系

重排 → 一定触发重绘
重绘 → 不一定触发重排
​
代价:重排 > 重绘 > 合成

🟩如何优化?

1. 批量修改样式,避免逐条修改

// ❌ 每次修改都触发重排,触发 3 次
el.style.width = '100px'
el.style.height = '100px'
el.style.margin = '10px'
​
// ✅ 只触发 1 次
el.className = 'new-class'
​
// ✅ 或者用 cssText 一次性修改
el.style.cssText = 'width:100px; height:100px; margin:10px'

2. 读写分离,避免强制同步布局

// ❌ 读写交替,每次读都强制触发重排
el.style.width = '100px'
console.log(el.offsetWidth)   // 强制重排
el.style.height = '100px'
console.log(el.offsetHeight)  // 强制重排
​
// ✅ 先读后写,只触发 1 次重排
const width = el.offsetWidth    // 先读
const height = el.offsetHeight  // 先读
el.style.width = width + 'px'  // 后写
el.style.height = height + 'px'

3. 离线操作 DOM

// ❌ 每次 appendChild 都触发重排
list.forEach(item => {
  const li = document.createElement('li')
  li.textContent = item
  ul.appendChild(li)  // 触发 n 次重排
})
​
// ✅ 用 DocumentFragment,只触发 1 次
const fragment = document.createDocumentFragment()
list.forEach(item => {
  const li = document.createElement('li')
  li.textContent = item
  fragment.appendChild(li)
})
ul.appendChild(fragment)  // 只触发 1 次

4. 用 display:none 隐藏后再操作

// 先隐藏(触发 1 次重排)
el.style.display = 'none'
​
// 做大量 DOM 操作(不触发重排,因为不在渲染树里)
el.style.width = '200px'
el.style.height = '300px'
el.appendChild(child)
// ...
​
// 再显示(触发 1 次重排)
el.style.display = 'block'
​
// 总共只触发 2 次重排,而不是 n 次

5. 用 transform 代替位移,触发合成而不是重排

// ❌ 改 top/left 触发重排
el.style.top = '100px'
el.style.left = '100px'
​
// ✅ 用 transform,只触发合成层,不重排不重绘
el.style.transform = 'translate(100px, 100px)'

6. 用 will-change 提升到合成层

/* 提前告诉浏览器这个元素会变化,单独放一层 */
.animated {
  will-change: transform, opacity;
}

注意不要滥用,每个合成层都占内存,用完记得移除。


🟩哪些 CSS 属性只触发合成(最快)

只改这些属性,不触发重排也不触发重绘,性能最好:

transform
opacity
filter
will-change

这也是为什么动画尽量用 transform 和 opacity 的原因。


🟩面试加分点

visibility: hidden 和 display: none 的区别?

visibility: hidden 只触发重绘(元素还占位置,只是不可见);display: none 触发重排(元素从渲染树中移除,不占位置)。

requestAnimationFrame 和重排有什么关系?

requestAnimationFrame 在每一帧绘制前执行,浏览器会把这一帧内所有的样式读写合并处理,天然避免了读写交替导致的强制同步布局,做动画时比 setTimeout 更高效。

现代框架(React/Vue)是怎么减少重排的?

通过虚拟 DOM + diff 算法,把多次 DOM 操作合并成一次批量更新,减少直接操作真实 DOM 的次数,从而减少重排重绘的触发频率。