为何说DOM操做很慢

一直都据说DOM很慢,要尽可能少的去操做DOM,因而就想进一步去探究下为何你们都会这样说,在网上学习了一些资料,这边整理出来。 css

首先,DOM对象自己也是一个js对象,因此严格来讲,并非操做这个对象慢,而是说操做了这个对象后,会触发一些浏览器行为,好比布局(layout)和绘制(paint)。下面主要先介绍下这些浏览器行为,阐述一个页面是怎么最终被呈现出来的,另外还会从代码的角度,来讲明一些很差的实践以及一些优化方案。 html

浏览器是如何呈现一张页面的

一个浏览器有许多模块,其中负责呈现页面的是渲染引擎模块,比较熟悉的有WebKit和Gecko等,这里也只会涉及这个模块的内容。 web

先用文字大体阐述下这个过程: chrome

  • 解析HTML,并生成一棵DOM tree
  • 解析各类样式并结合DOM tree生成一棵Render tree
  • 对Render tree的各个节点计算布局信息,好比box的位置与尺寸
  • 根据Render tree并利用浏览器的UI层进行绘制

其中DOM tree和Render tree上的节点并不是一一对应,好比一个display:none的节点就在会存在与DOM tree上,而不会出如今Render tree上,由于这个节点不须要被绘制。 编程

上图是Webkit的基本流程,在术语上和Gecko可能会有不一样,这里贴上Gecko的流程图,不过文章下面的内容都会统一使用Webkit的术语。 数组

影响页面呈现的因素有许多,好比link的位置会影响首屏呈现等。但这里主要集中讨论与layout相关的内容。 浏览器

paint是一个耗时的过程,然而layout是一个更耗时的过程,咱们没法肯定layout必定是自上而下或是自下而上进行的,甚至一次layout会牵涉到整个文档布局的从新计算。 缓存

可是layout是确定没法避免的,因此咱们主要是要最小化layout的次数。 app

什么状况下浏览器会进行layout

在考虑如何最小化layout次数以前,要先了解何时浏览器会进行layout。 dom

layout(reflow)通常被称为布局,这个操做是用来计算文档中元素的位置和大小,是渲染前重要的一步。在HTML第一次被加载的时候,会有一次layout以外,js脚本的执行和样式的改变一样会致使浏览器执行layout,这也是本文的主要要讨论的内容。

通常状况下,浏览器的layout是lazy的,也就是说:在js脚本执行时,是不会去更新DOM的,任何对DOM的修改都会被暂存在一个队列中,在当前js的执行上下文完成执行后,会根据这个队列中的修改,进行一次layout。

然而有时但愿在js代码中马上获取最新的DOM节点信息,浏览器就不得不提早执行layout,这是致使DOM性能问题的主因。

以下的操做会打破常规,并触发浏览器执行layout:

  • 经过js获取须要计算的DOM属性
  • 添加或删除DOM元素
  • resize浏览器窗口大小
  • 改变字体
  • css伪类的激活,好比:hover
  • 经过js修改DOM元素样式且该样式涉及到尺寸的改变

咱们来经过一个例子直观的感觉下:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Read
var h1 = element1 . clientHeight ;
 
// Write (invalidates layout)
element1 . style . height = ( h1 * 2 ) + 'px' ;
 
// Read (triggers layout)
var h2 = element2 . clientHeight ;
 
// Write (invalidates layout)
element2 . style . height = ( h2 * 2 ) + 'px' ;
 
// Read (triggers layout)
var h3 = element3 . clientHeight ;
 
// Write (invalidates layout)
element3 . style . height = ( h3 * 2 ) + 'px' ;

这里涉及一个属性clientHeight,这个属性是须要计算获得的,因而就会触发浏览器的一次layout。咱们来利用chrome(v47.0)的开发者工具看下(截图中的timeline record已经通过筛选,仅显示layout):

上面的例子中,代码首先修改了一个元素的样式,接下来读取另外一个元素的clientHeight属性,因为以前的修改致使当前DOM被标记为脏,为了保证能准确的获取这个属性,浏览器会进行一次layout(咱们发现chrome的开发者工具良心的提示了咱们这个性能问题)。

优化这段代码很简单,预先读取所须要的属性,在一块儿修改便可。

JavaScript
1
2
3
4
5
6
7
8
9
// Read
var h1 = element1 . clientHeight ;   
var h2 = element2 . clientHeight ;   
var h3 = element3 . clientHeight ;
 
// Write (invalidates layout)
element1 . style . height = ( h1 * 2 ) + 'px' ;   
element2 . style . height = ( h2 * 2 ) + 'px' ;   
element3 . style . height = ( h3 * 2 ) + 'px' ;

看下此次的状况:

下面再介绍一些其余的优化方案。

最小化layout的方案

上面提到的一个批量读写是一个,主要是由于获取一个须要计算的属性值致使的,那么哪些值是须要计算的呢?

这个连接里有介绍大部分须要计算的属性:http://gent.ilcore.com/2011/03/how-not-to-trigger-layout-in-webkit.html

再来看看别的状况:

面对一系列DOM操做

针对一系列DOM操做(DOM元素的增删改),能够有以下方案:

  • documentFragment
  • display: none
  • cloneNode

好比(仅以documentFragment为例):

JavaScript
1
2
3
4
5
6
7
var fragment = document . createDocumentFragment ( ) ;   
for ( var i = 0 ; i < items . length ; i ++ ) {   
   var item = document . createElement ( "li" ) ;
   item . appendChild ( document . createTextNode ( "Option " + i ) ;
   fragment . appendChild ( item ) ;
}
list . appendChild ( fragment ) ;

这类优化方案的核心思想都是相同的,就是先对一个不在Render tree上的节点进行一系列操做,再把这个节点添加回Render tree,这样不管多么复杂的DOM操做,最终都只会触发一次layout。

面对样式的修改

针对样式的改变,咱们首先须要知道并非全部样式的修改都会触发layout,由于咱们知道layout的工做是计算RenderObject的尺寸和大小信息,那么我若是只是改变一个颜色,是不会触发layout的。

这里有一个网站CSS triggers,详细列出了各个CSS属性对浏览器执行layout和paint的影响。

像下面这种状况,和上面讲优化的部分是同样的,注意下读写便可。

JavaScript
1
2
3
4
5
elem . style . height = "100px" ; // mark invalidated  
elem . style . width = "100px" ;   
elem . style . marginRight = "10px" ;
 
elem . clientHeight // force layout here

可是要提一下动画,这边讲的是js动画,好比:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
function animate ( from , to ) {   
   if ( from === to ) return
 
   requestAnimationFrame ( function ( ) {
     from += 5
     element1 . style . height = from + "px"
     animate ( from , to )
   } )
}
 
animate ( 100 , 500 )

动画的每一帧都会致使layout,这是没法避免的,可是为了减小动画带来的layout的性能损失,能够将动画元素绝对定位,这样动画元素脱离文本流,layout的计算量会减小不少。

使用requestAnimationFrame

任何可能致使重绘的操做都应该放入requestAnimationFrame

在现实项目中,代码按模块划分,很难像上例那样组织批量读写。那么这时能够把写操做放在requestAnimationFrame的callback中,统一让写操做在下一次paint以前执行。

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Read
var h1 = element1 . clientHeight ;
 
// Write
requestAnimationFrame ( function ( ) {   
   element1 . style . height = ( h1 * 2 ) + 'px' ;
} ) ;
 
// Read
var h2 = element2 . clientHeight ;
 
// Write
requestAnimationFrame ( function ( ) {   
   element2 . style . height = ( h2 * 2 ) + 'px' ;
} ) ;

能够很清楚的观察到Animation Frame触发的时机,MDN上说是在paint以前触发,不过我估计是在js脚本交出控制权给浏览器进行DOM的invalidated check以前执行。

其余注意点

除了因为触发了layout而致使性能问题外,这边再列出一些其余细节:

缓存选择器的结果,减小DOM查询。这里要特别体下HTMLCollection。HTMLCollection是经过document.getElementByTagName获得的对象类型,和数组类型很相似可是每次获取这个对象的一个属性,都至关于进行一次DOM查询:

JavaScript
1
2
3
4
var divs = document . getElementsByTagName ( "div" ) ;   
for ( var i = 0 ; i < divs . length ; i ++ ) {    //infinite loop  
   document . body . appendChild ( document . createElement ( "div" ) ) ;
}

好比上面的这段代码会致使无限循环,因此处理HTMLCollection对象的时候要最些缓存。

另外减小DOM元素的嵌套深度并优化css,去除无用的样式对减小layout的计算量有必定帮助。

在DOM查询时,querySelector和querySelectorAll应该是最后的选择,它们功能最强大,但执行效率不好,若是能够的话,尽可能用其余方法替代。

下面两个jsperf的连接,能够对比下性能。

https://jsperf.com/getelementsbyclassname-vs-queryselectorall/162
http://jsperf.com/getelementbyid-vs-queryselector/218

本身对View层的想法

上面的内容理论方面的东西偏多,从实践的角度来看,上面讨论的内容,正好是View层须要处理的事情。已经有一个库FastDOM来作这个事情,不过它的代码是这样的:

JavaScript
1
2
3
4
5
6
7
fastdom . read ( function ( ) {   
   console . log ( 'read' ) ;
} ) ;
 
fastdom . write ( function ( ) {   
   console . log ( 'write' ) ;
} ) ;

问题很明显,会致使callback hell,而且也能够预见到像FastDOM这样的imperative的代码缺少扩展性,关键在于用了requestAnimationFrame后就变成了异步编程的问题了。要让读写状态同步,那必然须要在DOM的基础上写个Wrapper来内部控制异步读写,不过都到了这份上,感受能够考虑直接上React了……

总之,尽可能注意避免上面说到的问题,但若是用库,好比jQuery的话,layout的问题出在库自己的抽象上。像React引入本身的组件模型,用过virtual DOM来减小DOM操做,并能够在每次state改变时仅有一次layout,我不知道内部有没有用requestAnimationFrame之类的,感受要作好一个View层就挺有难度的,以后准备学学React的代码。但愿本身一两年后会过来再看这个问题的时候,能够有些新的看法。

相关文章
相关标签/搜索