回流与重绘
浏览器渲染过程中的回流和重绘机制,以及如何减少性能损耗
问题
什么是回流和重绘?什么场景下会触发?如何减少回流和重绘?
解答
基本概念
在浏览器渲染页面时,每个元素都是一个盒子,渲染过程涉及两个关键步骤:
回流(Reflow):布局引擎计算每个盒子在页面上的大小和位置。
重绘(Repaint):根据盒子的位置、大小等属性,浏览器绘制元素的视觉样式。
浏览器渲染流程
- 解析 HTML 生成 DOM 树,解析 CSS 生成 CSSOM 树
- 将 DOM 树和 CSSOM 树结合,生成渲染树(Render Tree)
- Layout(回流):根据渲染树计算节点的几何信息(位置、大小)
- Painting(重绘):根据几何信息得到节点的绝对像素
- Display:将像素发送给 GPU 展示在页面上
触发回流的场景
当页面布局和几何信息发生变化时会触发回流:
- 添加或删除可见的 DOM 元素
- 元素的位置发生变化
- 元素的尺寸发生变化(外边距、内边距、边框、宽高等)
- 内容发生变化(文本变化或图片替换)
- 页面初始渲染
- 浏览器窗口尺寸变化
特别注意,获取以下属性会强制触发回流:
offsetTop、offsetLeft、offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight、getComputedStyle()
这些属性需要通过即时计算得到,浏览器会强制清空队列并触发回流来返回正确的值。
触发重绘的场景
回流必然触发重绘,但重绘不一定触发回流。
当 DOM 修改只影响样式而不影响几何属性时,只会触发重绘:
- 颜色修改
- 文本方向修改
- 阴影修改
减少回流和重绘的方法
1. 批量修改样式
避免多次单独修改样式,使用 class 合并修改:
// 不推荐:多次触发渲染
const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
/* 推荐:使用 class 合并样式 */
.basic_style {
width: 100px;
height: 200px;
border: 10px solid red;
color: red;
}
container.className = 'basic_style'
2. 缓存布局信息
避免在循环中多次读取布局属性:
// 不推荐:每次循环都触发回流
const el = document.getElementById('el')
for(let i = 0; i < 10; i++) {
el.style.top = el.offsetTop + 10 + "px"
el.style.left = el.offsetLeft + 10 + "px"
}
// 推荐:缓存属性值
const el = document.getElementById('el')
let offLeft = el.offsetLeft
let offTop = el.offsetTop
for(let i = 0; i < 10; i++) {
offLeft += 10
offTop += 10
}
el.style.left = offLeft + "px"
el.style.top = offTop + "px"
3. 离线操作 DOM
通过 display: none 隐藏元素后再操作,完成后再显示:
let container = document.getElementById('container')
container.style.display = 'none'
// 进行多次 DOM 操作
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.display = 'c9s3v'
4. 使用 DocumentFragment
批量插入节点时使用 DocumentFragment,一次性插入:
const fragment = document.createDocumentFragment()
for(let i = 0; i < 10; i++) {
const li = document.createElement('li')
fragment.appendChild(li)
}
document.getElementById('list').appendChild(fragment)
5. 其他优化建议
- 动画元素使用
position: fixed或absolute脱离文档流 - 避免使用 table 布局
- 使用 CSS3 硬件加速(transform、opacity、filters)
- 避免使用 CSS 的 JavaScript 表达式
关键点
- 回流计算元素几何信息,重绘绘制元素样式,回流必然触发重绘
- 读取 offset、scroll、client 等属性会强制触发回流
- 使用 class 批量修改样式,避免多次单独操作
- 缓存布局信息,减少重复读取
- 通过
display: none或 DocumentFragment 进行离线 DOM 操作
目录