理解回流和重绘对开发过程有什么指导意义?

277 阅读6分钟

浏览器回流重绘在开发的时候要注意什么点?什么样的代码会引起回流重绘?如何避免影响较大的回流重绘

下面就来看看

前置知识

  1. 浏览器解析 HTML(标记化、建树算法),生成 DOM 树,解析 CSS,生成 CSSOM 树
  2. DOM 和 CSSOM 合并就产生了渲染树(Render Tree),期间会进行计算节点的坐标位置 值得注意的是,这棵渲染树中包含可见元素,对于 head 标签和设置了 display: none 的元素,将不会 被放入其中。
  3. 依据 render tree 进行渲染绘制到屏幕上

    在渲染过程中浏览器也遇到了首屏问题,或者页面过大的问题。对此浏览器的优化方式是:图层分块解决页面过大问题;首次绘制采用低分辨率的图片,等到页面加载完毕再替换成高分辨率的图片解决首次绘制图块慢的问题;其中的合成线程会选择视口附近的图块,来进行渲染。都不会直接渲染整个网站的资源,对于渲染有需求的场景可以采用类似的优化策略,比如:大数据的表格

回流(Reflow)

浏览器重新计算元素的几何属性和位置的过程

回流时会计算节点的确切位置和几何属性,这个过程是非常消耗性能的

  • 触发时机
    • 页面首次渲染
    • DOM 元素几何属性变化,常见的几何属性有 widthheightpaddingmarginlefttopborder 等等, 这个很好理解
    • DOM 树结构变化:添加节点、删除节点或移动节点
    • 读写 offsetscrollclient 等属性
    • 调用 window.getComputedStyle 方法
    • 浏览器窗口大小变化

重绘(Repaint)

浏览器重新绘制元素外观的过程,当 DOM 的修改导致了样式的变化,且没有影响几何属性的时候,会导致重绘 (repaint)

  • 触发时机
    • 修改元素的背景颜色、文字颜色、边框颜色等,没有影响几何属性的时候,例如:colorvisibilityfilters

性能的影响

回流必将引起重绘,重绘不一定会引起回流

有时即使仅仅回流单一一个元素,他的父元素和子元素也会因此而触发回流,这是因为回流过程中,浏览器会从该节点递归向上回溯,然后再递归向下,这样就会导致整个渲染树的节点都会受到影响

浏览器对于频繁的回流或者重绘操作会有优化策略,通过维护一个队列,把所有引起回流和重绘的操作放入到队列中,如果队列中的任务达到一定的数量或者时间间隔,浏览器会将队列中的任务合并为一次回流或者重绘,以此来减少回流和重绘的次数,提高性能

但是有时候浏览器会强制触发回流,如下:

const element = document.getElementById('box')
for (let i = 0; i < 100; i++) {
    element.style.left = `${i}px`     // 每次循环都会触发回流
    console.log(element.offsetHeight)  // 强制触发回流
}

这段代码先调用了改变元素几何属性的方法,随后调用了读取元素几何属性的方法。正常来说浏览器会将多个触发回流的操作合并为一次回流,但读取元素几何属性的操作,浏览器会强制触发回流以确保获取精准的位置信息,所以这段代码会触发 100 次回流

我们需要避免频繁修改元素的几何属性后立马读取的操作

对开发过程有什么指导意义

css

  • 避免使用 table 布局,table 布局可能会触发多次回流,可以使用 flex 布局或者 grid 布局代替
  • 对于复杂动画,可以使用 position: fixed 或者 position: absolute 来脱离文档流,避免频繁回流
  • 避免使用 css 表达式,css 表达式会在每次回流时重新计算(比如:calc()
  • css3 硬件加速(GPU 加速)来减少回流和重绘,可以通过 will-change 属性或者 transform: translateZ(0) 来开启硬件加速。这可以让 transformopacityfilter 属性不会触发回流和重绘

常见的触发硬件加速的 css 属性:

  1. transform
  2. opacity
  3. filters
  4. Will-change

GPU 擅长处理位图数据,而且是单独的一个合成线程,没有占用主线程的资源,即使主线程卡住了,效果依然能够流畅地展示。使用之后可以提升动画的性能,但是也会带来一些问题,比如:会消耗更多的内存,会导致更多的电量消耗,会导致更多的 GPU 资源消耗,所以在使用硬件加速的时候需要权衡

JavaScript

  • 避免频繁使用 style,而是通过修改 class 来修改样式
  • 避免频繁操作 DOM,可以通过 DocumentFragment 来操作(使用 DocumentFragment 能解决直接操作 DOM 引发大量回流的问题,因为所有的节点会被一次插入到文档中,而这个操作仅发生一个重渲染的操作)

示例代码:

// 一般操作
let app = document.querySelector('.app')
  for(let i = 0;i<5;i++){
     let div = document.createElement('li')
     div.setAttribute('class','item')
     div.innerText = 6666
     app.appendChild(div)
  }

// 使用 DocumentFragment
let app = document.querySelector('.app')
let fragement = document.createDocumentFragment()
  for(let i = 0;i<5;i++){
     let div = document.createElement('li')
     div.setAttribute('class','item')
     div.innerText = 6666
     fragement.appendChild(div)
  }
app.appendChild(fragement)

DocumentFragment 节点不属于文档树,存在于内存中,并不在 DOM 中,将子元素插入到文档片段中时不会引起页面回流,因此使用 DocumentFragment 可以起到性能优化的作用

但现代浏览器会使用队列来储存多次修改,进行优化,就这个优化方式,效果并不是很明显,不用优先考虑

  • 可以先为元素设置 display: none,操作结束后再把它显示出来。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘

  • 避免频繁读取元素的几何属性,可以通过一次性读取多个属性,或者使用 getBoundingClientRect() 方法来获取元素的位置信息

  • 避免布局抖动,布局抖动是指在一次事件循环中多次读取和写入元素的几何属性

总结

大部分优化操作在我们日常开发中都不会太过注意,性能问题在还没遇到的时候不用过多考虑

需要避免的一个明显的一个影响性能的操作是:

  • 避免频繁修改元素的几何属性后立马读取的操作