求知若饥,虚心若愚。
浏览器加载完用户请求的页面之后会解析生成两个内部数据结构—-DOM树和渲染树。
图1:浏览器显示页面抽象图
DOM树是页面文档的结构表示,当DOM树构建完成时,浏览器开始构建渲染树。
DOM树中的每一个可见的节点(包括visibility=hidden的元素)在渲染树中至少存在一个对应的节点(display值为none的元素在渲染树中没有对应的节点)。所以渲染树是文档的可视化表示,这棵树是为了正确的绘制文档内容而建立。
渲染树中的元素可称之为渲染对象。每个渲染对象用一个和该节点的CSS盒模型相对应的矩形区域来表示,它包含诸如宽、高和位置之类的几何信息。盒模型的类型受该节点相关的display样式属性的影响。
一个渲染对象知道怎么布局及绘制自己和它的子元素。
当DOM的变化影响了元素的几何属性(宽或者高等等),浏览器需要重新计算元素的几何属性,同样其他元素的几何属性和位置也有可能会因此受到影响。
浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树,这个过程称为“重排”。完成重排后,浏览器会重新绘制受到影响的部分到屏幕,该过程称为“重绘”。
由于浏览器的流布局,对渲染树的计算通常只需要遍历一次就可以完成。
并不是所有的DOM变化都会影响几何属性,比如改变一个元素的背景色并不会影响元素的宽和高,这种情况下只会发生重绘。
现代浏览器大都利用了GPU来加速网页渲染。GPU是专用于图形渲染的芯片,它很擅长做如下事情:
浏览器在渲染一个页面时,会做如下的一些工作:
Webkit类浏览器满足以下任意情况就会创建图层:
如果图层中某个元素需要重绘,那么整个图层都需要重绘。所以图层DOM节点越少,重排和重绘的性能越好。
比如一个图层包含很多节点,其中有个gif图,gif图的每一帧都会重绘整个图层的其他节点,然后生成最终的图层位图。这时候最好强制gif图片位于自己的一个图层中去,绝大部分浏览器自己会为CSS3动画的节点创建新的图层。
利用translateZ(0)或者translate3d(0,0,0)开启硬件加速可以使一个元素属于自己的一个层。当有了单独的层之后,此元素的重排、重绘操作将只需要更新自己,不用影响到别人。可以看做是局部更新,所以开启了硬件加速的动画会变得流畅很多。
简化一下上述过程,每一帧动画浏览器可能需要做如下工作:
在做页面时需要控制图层的数量,因为层的创建和更新都会消耗内存。控制图层重绘的次数是为了减少位图更新的次数。每次位图的更新,线程就需要提交新的位图给GPU,频繁地更新位图也会拖慢GPU的效率。
不当的重排和重绘代价有多大?看看下面这个例子就知道了:
1 | var times=2000; |
从上面的例子可以看出一次改变DOM内容和多次改变DOM内容性能相差了千倍,这是因为每次改变DOM内容都会引起浏览器渲染引擎的重排和重绘,而Cordova App编程消耗的很多性能瓶颈也正是在这里。
从上面还能计算出每次重排与重绘的时间约为8.3ms((16603-11)/2000)。从而可以算出CodeC 2000次字符串拼接用了大约3ms。
总的来说,多次访问DOM对于重排和重绘来说,耗时不值得一提,JS本身的一些计算更不值得一提。
总的来说,每次DOM元素的几何属性或者内容改变时将会导致重排和重绘同时发生;每次DOM元素的非几何样式发生改变时会重绘,大致有以下几种情况:
上面这些改变有的会引起全局重排与重绘,有的只会引起局部或者叫增量的重排与重绘。有的只会引起重绘,有的会引起重排与重绘都发生,下面具体看看会引起重排和重绘的CSS属性。
1.盒子模型相关属性会触发重排:
1 | .model{ |
定位属性及浮动也会触发重排:
1 | .model{ |
改变节点内部文字样式也会触发重排:
1 | .model{ |
可以看到,它们的特点就是可能修改整个节点的大小或位置,所以会触发重排。
修改时只触发重绘的属性有:
1 | .model{ |
请看下面代码:
1 | var el=document.getElementById('myDiv'); |
元素的几何样式改变了3次,每次改变都会引起重排和重绘,所以总共有4次重排重绘的过程。浏览器会通过队列缓存来合并3次为一次修改来优化重排与重绘,但是,有时候你可能会不知不觉强制刷新队列并要求任务立即执行。
获取布局属性信息的操作会导致队列刷新,例如:
1 | offsetTop、offsetLeft、offsetWidth、offsetHeight, |
将上面的代码稍加修改:
1 | var el=document.getElementById('myDiv'); |
因为offsetTop属性需要返回最新的布局信息,因此浏览器不得不清空渲染队列立马触发重排以返回正确的值(就算渲染队列中的布局改变信息和我们要获取的没有关系),所以上面的代码,前两次的操作会缓存在渲染队列中等待处理,但是一旦offsetTop属性被请求了,队列就会立即执行,所以会有2次重排与重绘,所以尽量不要在布局信息改变时做查询。
1.集中修改样式
我们还是看上面的这段代码:
1 | var el=document.getElementById('myDiv'); |
3个样式属性被改变,每一个都会影响元素的几何结构。虽然现代浏览器都做了优化,只会引起一次重排,但是像上文一样,如果一个即时的CSS几何属性被请求,那么就会强制刷新队列,而且这段代码4次访问DOM,一个很显然的优化策略就是把它们的操作合成一次,这样只会重排和重绘DOM一次:
1 | var el=document.getElementById('myDiv'); |
2.缓存元素几何属性值
对于元素几何属性值,如果需要多次访问则可以在一次访问时先储存到变量中,之后都使用该变量,这样可以避免每次读取属性时无意间造成浏览器的渲染。
1 | var width=el.offsetWidth; |
3.fragment元素的应用
看如下代码,考虑一个问题:
1 | <ul id='framework'> |
如果要添加内容为jquery-mobile、sencha-touch两个选项,可以用如下代码实现:
1 | var liWrap=document.getElementById('framework'); |
但是很显然,上面的代码会导致重排重绘两次,怎么优化?
隐藏的元素不在渲染树中,我们可以先把id为framework的ul元素隐藏(display=none),然后添加li元素,最后再显示,但是实际操作中可能会出现闪动,所以不推荐使用。
删除的元素不在渲染树中,我们可以先把id为framework的ul元素删除,然后添加li元素,最后再显示,但是实际操作中也可能会出现闪动,所以不推荐使用。
但是我们还可以使用fragment这个元素,利用如下代码就OK:
1 | var liWrap=document.getElementById('framework'); |
文档片段是个轻量级的Document对象,它的设计初衷就是为了完成这类重复大量的更新和移动DOM节点的。
文档片段的一个语法特性是当你附加一个片断到节点时,实际上被添加的是该片断的子节点,而不是片断本身。只触发了一次重排,而且只访问了一次实时的DOM。