重排、重绘

152 阅读6分钟

在讨论重排与重绘之前,我们要知道:

  1. 浏览器使用流式布局模型 (Flow Based Layout)。
  2. 浏览器会把HTML解析成DOM,把CSS解析成CSSOMDOMCSSOM合并就产生了Render Tree
  3. 有了RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上。
  4. 由于浏览器使用流式布局,对Render Tree的计算通常只需要遍历一次就可以完成,但table及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用table布局的原因之一。

一句话:重排必将引起重绘,重绘不一定会引起重排。

  • 我们首先来看一下,浏览器的渲染原理,看一下,它们分别担任了什么工作?

一、浏览器渲染原理

00d75d202245bbcb64b357d00c89e586.png结合上图,一个完整的渲染流程大致可总结为如下几个步骤:

  1. HTML被HTML解析器解析成DOM Tree
  2. CSS则被CSS解析器解析成CSSOM Tree
  3. DOM Tree和CSSOM Tree解析完成后,被附加到一起,形成渲染树(Render Tree)
  4. 布局,根据渲染树计算每个节点的几何信息生成布局树(Layout Tree)
  5. 对布局树进行分层,并生成分层树(Layer Tree)
  6. 为每个图层生成绘制列表
  7. 渲染绘制(Paint)。根据计算好的绘制列表信息绘制整个页面,并将其提交到合成线程
  8. 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图,发送绘制图块命令 DrawQuad 给浏览器进程
  9. 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上

重排 (Reflow)

Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为重排。

会导致重排的操作:

  • 页面首次渲染
  • 浏览器窗口大小发生改变
  • 元素尺寸或位置发生改变
  • 元素内容变化(文字数量或图片大小等等)
  • 元素字体大小变化
  • 添加或者删除可见DOM元素
  • 激活CSS伪类(例如::hover
  • 查询某些属性或调用某些方法

一些常用且会导致重排的属性和方法:

  • clientWidthclientHeightclientTopclientLeft
  • offsetWidthoffsetHeightoffsetTopoffsetLeft
  • scrollWidthscrollHeightscrollTopscrollLeft
  • scrollIntoView()scrollIntoViewIfNeeded()
  • getComputedStyle()
  • getBoundingClientRect()
  • scrollTo()

重绘 (Repaint)

当页面中元素样式的改变并不影响它在文档流中的位置时(例如:colorbackground-colorvisibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

性能影响

重排比重绘的代价要更高。

有时即使仅仅重排一个单一的元素,它的父元素以及任何跟随它的元素也会产生重排。

现代浏览器会对频繁的重排或重绘操作进行优化:

浏览器会维护一个队列,把所有引起重排和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次重排和重绘变成一次。

当你访问以下属性或方法时,浏览器会立刻清空队列:

  • clientWidthclientHeightclientTopclientLeft
  • offsetWidthoffsetHeightoffsetTopoffsetLeft
  • scrollWidthscrollHeightscrollTopscrollLeft
  • widthheight
  • getComputedStyle()
  • getBoundingClientRect()

因为队列中可能会有影响到这些属性或方法返回值的操作,即使你希望获取的信息与队列中操作引发的改变无关,浏览器也会强行清空队列,确保你拿到的值是最精确的。

六、优化策略

6.1 减少DOM操作

  1. 最小化DOM访问次数,尽量缓存访问DOM的样式信息,避免过度触发重排。
  2. 如果在一个局部方法中需要多次访问同一个dom,可以在第一次获取元素时用变量保存下来,减少遍历时间。
  3. 用事件委托来减少事件处理器的数量。
  4. 用querySelectorAll()替代getElementByXX()。
  • querySelectorAll():获取静态集合,通过函数获取元素之后,元素之后的改变并不会影响之前获取后存储到的变量。也就是获取到元素之后就和html中的这个元素没有关系了
  • getElementByXX():获取动态集合,通过函数获取元素之后,元素之后的改变还是会动态添加到已经获取的这个元素中。换句话说,通过这个方法获取到元素存储到变量的时候,以后每一次在Javascript函数中使用这个变量的时候都会再去访问一下这个变量对应的html元素。

6.2 减少重排

  1. 放弃传统操作DOM的时代,基于vue/react开始数据影响视图模式。
  2. 避免设置大量的style内联属性,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow,所以最好是使用class属性。
  3. 不要使用table布局,因为table中某个元素一旦触发了reflow,那么整个table的元素都会触发reflow。那么在不得已使用table的场合,可以设置table-layout:auto;或者是table-layout:fixed这样可以让table一行一行的渲染,这种做法也是为了限制reflow的影响范围。
  4. 尽量少使用display:none可以使用visibility:hidden代替,display:none会造成重排,visibility:hidden只会造成重绘。
  5. 使用resize事件时,做防抖和节流处理。
  6. 分离读写操作(现代的浏览器都有渲染队列的机制)
  • 分离读写减少重排的原理
<style>  #box{    width:100px;    height:100px;    border:10px solid #ddd;  }</style><body>  <div id="box"></div>  <script>    //读写分离,一次重排    let box = document.getElementById('box')    box.style.width='200px';//(写)js改变样式,加入渲染队列中,顿一下,查看下一行是否还是修改样式,如果是则再加入到渲染队列,一直到下一行代码不是修改样式为止    box.style.height='200px';//(写)    box.style.margin='10px';//(写)    console.log(box.clientWidth);//(读)  </script></body>
<script>    //没做到读写分离,两次重排    box.style.width='200px';//(写)js改变样式,加入渲染队列中,顿一下,下一行不是修改样式的代码,浏览器就会直接渲染一次(重排)    console.log(box.clientWidth);//(读)    box.style.height='200px';//(写)    box.style.margin='10px';//(写)  </script>
  1. 缓存布局信息
<script>    //两次重排 ’写‘操作中包含clientWidth属性,会刷新渲染队列    box.style.width = box.clientWidth +10 +’px’;    box.style.height= box.clientHeight +10 +’px’</script>
<script>    let a=box.clientWidth //(读)缓存布局信息    let b=box.clientHeight//(读)缓存布局信息    //一次重排     box.style.width = a+10 +’px’;(写)    box.style.height=  b+10 +’px’(写)</script>

注意:offsetTop、offsetLeft、offsetWidth、offsetHeight、clientTop、clientWidth、clientHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、getComputedStyle、currentStyle...会刷新渲染队列。当下一行代码有这些时,即使下一行是修改样式,也会直接发生重排。

6.3 css及优化动画

  1. 少用css表达式
  2. 样式集中改变(批量处理) 减少通过JavaScript代码修改元素样式,尽量使用修改class名方式操作样式或动画;
  3. 可以把动画效果应用到position属性为absolute或fixed的元素上,这样对其他元素影响较小
  4. 动画实现的速度的选择:牺牲平滑度换取速度。比如实现一个动画,以1个像素为单位移动这样最平滑,但是reflow就会过于频繁,大量消耗CPU资源,如果以3个像素为单位移动则会好很多。
  5. 开启css3动画硬件加速(GPU加速)把渲染计算交给GPU。(能用transform做的就不要用其他的,因为transform可以开启硬件加速,而硬件加速可以规避重排。直接跳过重排、重绘,走合成进程)
box.style.left='100px' //向右移动100px,一次重排  box.style.ctransform='translateX(200)' //向右移动200px,不会引发重排