回流与重绘

浏览器渲染过程中的回流和重绘机制,以及如何减少性能损耗

问题

什么是回流和重绘?什么场景下会触发?如何减少回流和重绘?

解答

基本概念

在浏览器渲染页面时,每个元素都是一个盒子,渲染过程涉及两个关键步骤:

回流(Reflow):布局引擎计算每个盒子在页面上的大小和位置。

重绘(Repaint):根据盒子的位置、大小等属性,浏览器绘制元素的视觉样式。

浏览器渲染流程

  1. 解析 HTML 生成 DOM 树,解析 CSS 生成 CSSOM 树
  2. 将 DOM 树和 CSSOM 树结合,生成渲染树(Render Tree)
  3. Layout(回流):根据渲染树计算节点的几何信息(位置、大小)
  4. Painting(重绘):根据几何信息得到节点的绝对像素
  5. Display:将像素发送给 GPU 展示在页面上

触发回流的场景

当页面布局和几何信息发生变化时会触发回流:

  • 添加或删除可见的 DOM 元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(外边距、内边距、边框、宽高等)
  • 内容发生变化(文本变化或图片替换)
  • 页面初始渲染
  • 浏览器窗口尺寸变化

特别注意,获取以下属性会强制触发回流:

offsetTopoffsetLeftoffsetWidthoffsetHeightscrollTopscrollLeftscrollWidthscrollHeightclientTopclientLeftclientWidthclientHeightgetComputedStyle()

这些属性需要通过即时计算得到,浏览器会强制清空队列并触发回流来返回正确的值。

触发重绘的场景

回流必然触发重绘,但重绘不一定触发回流。

当 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: fixedabsolute 脱离文档流
  • 避免使用 table 布局
  • 使用 CSS3 硬件加速(transform、opacity、filters)
  • 避免使用 CSS 的 JavaScript 表达式

关键点

  • 回流计算元素几何信息,重绘绘制元素样式,回流必然触发重绘
  • 读取 offset、scroll、client 等属性会强制触发回流
  • 使用 class 批量修改样式,避免多次单独操作
  • 缓存布局信息,减少重复读取
  • 通过 display: none 或 DocumentFragment 进行离线 DOM 操作