前言
repaint(重绘)和reflow(回流),其实是老生常谈的事情,一直从没正式写过总结,晚上偶然想起,不如简单写一下。
首先我们说下,浏览器的简单的渲染流程如下:
- 解析html构建DOM树 ----> 解析css构建CSS树:此时html和css被解析成树形的数据结构
- 构建Render树:将DOM树和CSS树结合形成的Render树
- 布局Render树:有了Render树,浏览器已经知道那些有哪些节点,各个节点的css定义和以及它们的从属关系,从而计算出每个节点在屏幕中的位置,经过Layout计算可见节点在设备视口(viewport)内的几何信息
- 绘制render树:按照已知Render树和布局的几何信息,通过GPU把内容画在屏幕上
这是大概的渲染流程。
reflow(回流)
那么在初次渲染完成后,如果因为一些修改,浏览器为了重新渲染部分或整个页面,重新计算页面元素位置和几何结构的进程叫做reflow
回流会从html这个根节点开始递归往下,依次计算所有可见节点的几何信息,且回流是无可避免的
回流其实就是当浏览器中某个部分的页面布局或者几何信息发生变化时,就又会重新执行浏览器渲染的Layout和Painting以及Display过程
什么操作会导致reflow?
- 改变窗口大小
- 改变文字大小
- 添加/删除样式表
- 内容的改变,(用户在输入框中写入内容也会)
- 激活伪类,如:hover
- 操作class属性
- 脚本操作DOM
- 计算offsetWidth和offsetHeight
- 设置style属性
reflow(回流)是导致DOM脚本执行效率低的关键因素之一,页面上任何一个节点触发了reflow,会导致它的子节点及祖先节点重新渲染。
所以说如何减少reflow次数,及DOM的层级,CSS的效率对refolw次数的影响,也是要学会优化代码的一个方面,尽量避免没必要的reflow。
所以平时在书写代码时需要注意:
- 尽量少采用style行内样式,使用className。style的每次修改都会触发reflow
- 限制reflow影响的范围,尽可能在低级的dom层级上触发reflow,不要通过父类去影响子类的样式,尽可能直接给子类添加className, 通过className查找修改
- 不要使用table布局,table中每个元素的改变都会触发reflow. 那么在不得已使用table的场合,可以设置table-layout:auto;或者是table-layout:fixed这样可以让table一行一行的渲染,这种做法也是为了限制reflow的影响范围
- 如果CSS里面有计算表达式,每次都会重新计算一遍,出发一次reflow
- 实现元素的动画,它的position属性,最好是设为absoulte或fixed,这样不会影响其他元素的布局
- 动画实现的速度的选择。比如实现一个动画,以1个像素为单位移动这样最平滑,但是reflow就会过于频繁,大量消耗CPU资源,如果以5个像素为单位移动则会好很多
repaint(重绘)
什么是repaint(重绘)
改变某个元素的背景色、文字颜色、边框颜色等不影响它周围或内部布局的属性时,就会重新执行浏览器的Painting和Display过程(元素的几何信息没有变),这就是repaint
只要是不引起几何位置的改变都只会重绘。
回流一定重绘,重绘不一定回流
回流是触发了浏览器渲染过程中的Layout,Paiting,Display,而重绘是触发了浏览器渲染过程中的Painting,Display。
回流的过程包括了重绘,如果触发了回流,那么必定重绘会随着浏览器渲染过程一起发生,所以回流一定重绘。而重绘的发生,并不会执行Layout,因此不一定会产生回流,所以重绘不一定回流。
总结
那么问题来了?display:none 和 visibility:hidden,分别会触发那个?
我们都知道display:none是直接从文档流中移除,不会存在于RenderTree里。而visibility:hidden只是隐藏,所占的空间大小都存在,存在于RenderTree里。所以操作display改变时,肯定会触发回流。操作visibility时,只会触发重绘。
每一次的回流与重绘都是非常消耗性能的,所以大多数浏览器引入了异步队列的形式,当改变元素样式时,并不会立刻回流或重绘一次,而是把这些操作加入到队列中,在经过至少一个浏览器刷新时间(16.6ms)或者队列中操作到达了一个阈值,才会清空队列,执行一次回流或重绘,这叫做异步重绘或增量异步重绘
那么如何减少回流和重绘呢?
css方面:
- 1:注重书写顺序
(1)定位属性:position display float left top right bottom overflow clear z-index
(2)自身属性:width height padding border margin background
(3)文字样式:font-family font-size font-style font-weight font-varient color
(4)文本属性:text-align vertical-align text-wrap text-transform text-indent text-decoration letter-spacing word-spacing white-space text-overflow
(5)css3中新增属性:content box-shadow border-radius transform……
举例:
width: 100px;
color: red;
position: absolute;
cssTree的解析渲染存在于之前浏览器渲染流程中的②③④步骤。在布局RenderTree的过程中,开始再次遍历解析css树,如果此时css的书写顺序为以上举例顺序,解析到position发现需要脱离文档流,必定导致layout重新计算,此时导致再次发生回流,重新渲染节点的几何位置,导致性能损耗和展示延迟,势必影响用户体验。
这里延伸一下,DomTree和CSSTREE的解析是循序渐进从根目录往下进行的,但是CSS的匹配规则却不是从左到右执行的,而是从右到左。其实也很好理解,就是一个匹配效率问题,从左到右的话,刚开始匹配父节点,然后纵深开始往下匹配子节点,如果进入很深的结构后,却没有匹配到子节点,说明匹配失败,需要重新匹配,这个流程的效率是很低的。所有从子节点开始匹配,只需要判断父节点是否符合。
这也从侧面说明一个问题,书写css的时候层级不要太深,一般不超过三层,否则也影响匹配效率
- 2:使用visibility代替display:none
- 3: 复杂动画效果,使用绝对定位让其脱离文档流,避免影响其他元素
- 4: 尽量修改末端的class, 减少影响
js方面
- 避免多次修改DOM样式
# 不推荐
const body = document.body;
body.style.width = 200 + 'px';
box.sthle.background = 'red';
# 推荐
const body = document.body;
body.style.cssText += 'width:200px;background:red;'
或者添加className进行控制
- 避免频繁操作DOM
操作Dom在所难免,可以使用以下方法来减少回流与重绘
1、使其脱离文档流
2、进行DOM操作
3、将其添加回文档流中
脱离文档流和添加回文档这两次回流是无可避免的,但是中间的DOM操作,则是在Render Tree之外进行的,因此不会产生任何的回流与重绘.
而脱离文档流,可以用两种方法:
1、display:none 使其节点不可见,脱离渲染树
2、documentFragment 文档片段
操作都很简单,就是display:none后或创建文档片段后对其进行DOM操作,之后再将其display:block和添加到文档中
# display
div.style.display = 'none' // 节点不可见,触发一次回流
div.style.width = 100 + 'px' // 此时为不可见节点,脱离了RenderTree,因此不会产生回流和重绘
div.style.display = 'block' // 节点可见,触发一次回流
# documentFragment
const fragment = document.createDocumentFragment() // 触发一次回流
// 对fargment 进行DOM操作
div.appendChild(fragment) // 将文档片段插进DOM中,触发一次回流
- 避免频繁操作引起回流的属性读取和方法执行
offsetTop、offsetLeft、offsetWidth、offsetHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
clientTop、clientLeft、clientWidth、clientHeight
width、height
getComputedStyle()
getBoundingClientRect
应该避免频繁的使用上述的属性和方法,因为它们都会强制渲染并刷新队列。
比如不要每次都直接读取document.body.offsetWidth而是应该存储到一个变量中const width=document.body.offsetWidth; 让其他代码块读取变量。