Blog icon indicating copy to clipboard operation
Blog copied to clipboard

简单粗暴理解重排与重绘

Open pekonchan opened this issue 6 years ago • 0 comments

在理解重排与重绘之前,首先要理解页面渲染的一个简单过程以及涉及到的一些基础知识

页面渲染的简单过程

  1. 浏览器把HTML源代码解析,并且创建一个DOM树(DOM tree)
  2. 浏览器把CSS源代码解析,并且创建一个CSSOM树
  3. 根据DOM树和CSSOM树生成渲染树(render tree
  4. 根据渲染树生成布局并将布局绘制到浏览器界面上

render tree和DOM树的关系可以这么描述:

DOM树可视部分会形成render tree,因为render tree也是基于样式的

如DOM树中的某些节点的样式是display:none的,那么该节点就不存在与render tree里了。

还有就是<header>标签内的信息由于浏览器页面可视区域不会显示出来,他们这些节点也不会存在于render tree里。

重排和重绘都会引起上述的第四步,要重新生成布局并绘制到浏览器上,此外,更主要的是ui线程和js线程是互斥的,重排重绘的频繁触发,引起频繁的ui渲染,会导致js执行变慢。

重排(reflow)

好了我们知道了一些背景知识后,来认识下重排。概念上理解:

改变构成渲染树的信息,导致部分渲染树节点或全部渲染树需要重新分析并计算尺寸位置,叫做重排

例子上形象地说:

文档原本的内容已经渲染好了之后,各个元素的位置和宽高等已经排列好了,但是当你修改了某个元素的几何样式之后,如宽变大了,就会导致其兄弟节点乃至其他节点的位置可能会发生变化,那么这种变化,就需要浏览器对收到影响的部分进行重新渲染重现给用户看

重绘(repaint)

改变元素的非尺寸位置样式,如color、visibility等,导致需要重新绘制新的样式到页面元素上,叫重绘。

重排必定会导致重绘,重绘不一定也会发生重排

触发场景

触发重排或重绘,前提是渲染树render tree发生了变化,即用于构成渲染树的信息发生变化,即css或html发生变化,这是反向推导。

重绘的场景很容易理解,从定义上我们就知道何时会触发了。

现在主要说说触发重排的场景

从概念上看,改变元素的几何属性就会触发重排,理论上改了一次就会触发一次(现在得益于现代浏览器的优化,而不会是改一次马上变一次了,后续细说)。

什么情况下会发生重排呢?

1. 页面首次渲染(这是必然会有一次重排的,无法避免)

2. 元素的尺寸和位置

3. 元素的内容发生变化,如input中输入文本、文本大小变化

4. 改变浏览器宽高大小(包括出现滚动条导致可视区域变化)

5. 添加或删除【可见】的DOM元素

6. 激活css伪类如:hover

7. 调用某些属性和方法导致清除浏览器优化机制,进行重排(见下方)

避免重排与重绘

重排与重绘是很耗性能的,不利于提升用户体验。所以我们要尽量去避免发生多次的重排与重绘,毕竟有时候我们页面交互定然需要发生必要的重排重绘,那我们能优化的就是降低他们发生的次数。

浏览器优化机制

在我们重视这个问题的时候,浏览器生厂商早就注意到这个问题了,因此他们对这个问题做了如下优化:

有一个队列,对需要进行重排重绘的操作注入到这个队列中,等到到达了设置的某个数量阈值或者时间,就会一次性把队列中的操作执行了,把原本多次需要进行重排重绘变成一次性操作了。

但是要记住,获取以下属性和方法时,就会主动清除掉上述说的“队列”,马上执行掉,因为要实时获取最精准的值

- offsetWidth、offsetHeight、clientWidth、clentHeight、scrollHeight、scrollWidth

- offsetTop、offsetLeft、clientTop、clientLeft、scrollTop、scrollLeft

- getComputedStyle() or currentStyle in IE

- getBoundingClientRect()

我们的代码处理

避免常调用几何属性等

像上小节说的调用那几个属性和方法,会马上执行掉队列中的操作,导致马上发生重排。所以为了让浏览器这个优化机制正常处理,就要避免发生改变了导致重排重绘的操作后马上调用上述属性和方法

用变量存储

如果你要获取某个上述罗列的属性,需要多次用到的话,建议用变量存储起来。如

var width = obj.offsetWidth;
用documentFragment创建文档片段

通过new DocumentFragment() 创建文档片段,然后所有DOM操作都在这个片段上执行,然后再插入回文档中,只会触发一次重排

改变样式替换类名或使用style.cssText

假设你需要对一个元素改变它的几个样式属性

obj.style.left = 0;
obj.style.width = 200px;
obj.style.height = 300px;

尽量避免上述写法,而是采用类名形式直接替换

obj.className += 'addNewClass'
.addNewClass {
    left: 0;
    width: 200px;
    height: 300px;
}

或使用CSSText

obj.style.cssText += '; left: 0;widht: 200px; height: 300px;'
将动画应用在绝对定位的元素中

将动画作用在脱离文档流的定位元素中(absolute / fixed),以减少对其他元素的影响

“离线”执行DOM操作

我们知道render tree主要是可视的DOM生成,而重排重绘也是应为render tree变化才引起的。所以我们可以对要进行一系列DOM操作的元素先设置display: none,让其“离线”,然后肆无忌惮地进行DOM操作,执行完之后再设置回来,以减少中途产生的重排重绘次数。

这里必然会产生两次重排重绘,一次是隐藏掉一次是显示出来的时候

避免使用table布局

table布局跟标准的文档流处理不一样,重排所花费的时间更长

某些css的替换
  • 尽量使用opacity(不会触发重绘)来替代visibility(会触发重绘)的效果。
  • 可考虑使用translate做位置的调整,避免直接修改top等方位属性,因为使用translate它会自己生成一个独立的图层,不会影响到别的图层重排重绘。
避免使用css表达式(如calc()

pekonchan avatar Aug 13 '19 10:08 pekonchan