在讨论重排与重绘之前,我们要知道:
- 浏览器使用流式布局模型 (Flow Based Layout)。
- 浏览器会把
HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合并就产生了Render Tree。 - 有了
RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上。 - 由于浏览器使用流式布局,对
Render Tree的计算通常只需要遍历一次就可以完成,但table及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用table布局的原因之一。
一句话:重排必将引起重绘,重绘不一定会引起重排。
- 我们首先来看一下,浏览器的渲染原理,看一下,它们分别担任了什么工作?
一、浏览器渲染原理
结合上图,一个完整的渲染流程大致可总结为如下几个步骤:
- HTML被HTML解析器解析成DOM Tree
- CSS则被CSS解析器解析成CSSOM Tree
- DOM Tree和CSSOM Tree解析完成后,被附加到一起,形成渲染树(Render Tree)
- 布局,根据渲染树计算每个节点的几何信息生成布局树(Layout Tree)
- 对布局树进行分层,并生成分层树(Layer Tree)
- 为每个图层生成绘制列表
- 渲染绘制(Paint)。根据计算好的绘制列表信息绘制整个页面,并将其提交到合成线程
- 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图,发送绘制图块命令 DrawQuad 给浏览器进程
- 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上
重排 (Reflow)
当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为重排。
会导致重排的操作:
- 页面首次渲染
- 浏览器窗口大小发生改变
- 元素尺寸或位置发生改变
- 元素内容变化(文字数量或图片大小等等)
- 元素字体大小变化
- 添加或者删除可见的
DOM元素 - 激活
CSS伪类(例如::hover) - 查询某些属性或调用某些方法
一些常用且会导致重排的属性和方法:
clientWidth、clientHeight、clientTop、clientLeftoffsetWidth、offsetHeight、offsetTop、offsetLeftscrollWidth、scrollHeight、scrollTop、scrollLeftscrollIntoView()、scrollIntoViewIfNeeded()getComputedStyle()getBoundingClientRect()scrollTo()
重绘 (Repaint)
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
性能影响
重排比重绘的代价要更高。
有时即使仅仅重排一个单一的元素,它的父元素以及任何跟随它的元素也会产生重排。
现代浏览器会对频繁的重排或重绘操作进行优化:
浏览器会维护一个队列,把所有引起重排和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次重排和重绘变成一次。
当你访问以下属性或方法时,浏览器会立刻清空队列:
clientWidth、clientHeight、clientTop、clientLeftoffsetWidth、offsetHeight、offsetTop、offsetLeftscrollWidth、scrollHeight、scrollTop、scrollLeftwidth、heightgetComputedStyle()getBoundingClientRect()
因为队列中可能会有影响到这些属性或方法返回值的操作,即使你希望获取的信息与队列中操作引发的改变无关,浏览器也会强行清空队列,确保你拿到的值是最精确的。
六、优化策略
6.1 减少DOM操作
- 最小化DOM访问次数,尽量缓存访问DOM的样式信息,避免过度触发重排。
- 如果在一个局部方法中需要多次访问同一个dom,可以在第一次获取元素时用变量保存下来,减少遍历时间。
- 用事件委托来减少事件处理器的数量。
- 用querySelectorAll()替代getElementByXX()。
- querySelectorAll():获取静态集合,通过函数获取元素之后,元素之后的改变并不会影响之前获取后存储到的变量。也就是获取到元素之后就和html中的这个元素没有关系了
- getElementByXX():获取动态集合,通过函数获取元素之后,元素之后的改变还是会动态添加到已经获取的这个元素中。换句话说,通过这个方法获取到元素存储到变量的时候,以后每一次在Javascript函数中使用这个变量的时候都会再去访问一下这个变量对应的html元素。
6.2 减少重排
- 放弃传统操作DOM的时代,基于vue/react开始数据影响视图模式。
- 避免设置大量的style内联属性,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow,所以最好是使用class属性。
- 不要使用table布局,因为table中某个元素一旦触发了reflow,那么整个table的元素都会触发reflow。那么在不得已使用table的场合,可以设置table-layout:auto;或者是table-layout:fixed这样可以让table一行一行的渲染,这种做法也是为了限制reflow的影响范围。
- 尽量少使用display:none可以使用visibility:hidden代替,display:none会造成重排,visibility:hidden只会造成重绘。
- 使用resize事件时,做防抖和节流处理。
分离读写操作(现代的浏览器都有渲染队列的机制)
- 分离读写减少重排的原理
<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>
缓存布局信息
<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及优化动画
- 少用css表达式
样式集中改变(批量处理)减少通过JavaScript代码修改元素样式,尽量使用修改class名方式操作样式或动画;- 可以把动画效果应用到position属性为absolute或fixed的元素上,这样对其他元素影响较小
动画实现的速度的选择:牺牲平滑度换取速度。比如实现一个动画,以1个像素为单位移动这样最平滑,但是reflow就会过于频繁,大量消耗CPU资源,如果以3个像素为单位移动则会好很多。开启css3动画硬件加速(GPU加速)把渲染计算交给GPU。(能用transform做的就不要用其他的,因为transform可以开启硬件加速,而硬件加速可以规避重排。直接跳过重排、重绘,走合成进程)
box.style.left='100px' //向右移动100px,一次重排 box.style.ctransform='translateX(200)' //向右移动200px,不会引发重排