🟩前端面试题:重绘与重排
🟩先理解浏览器渲染流程
要搞懂重绘重排,先要知道浏览器是怎么把代码变成画面的:
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 的次数,从而减少重排重绘的触发频率。