Naiteluo

home

high performance JavaScript notes 2

19 Aug 2012

Chapter 3 DOM Scripting DOM 编程

用脚本进行DOM操作的代价很昂贵,它是富Web应用中最常见的性能瓶颈。

这章主要涉及一下三类问题:

文档对象模型是一个语言无关的,用于操作XML和HTML文档的应用程序借口API。虽然它是语言无关的,但是在浏览器中的接口却是用JavaScript实现的。浏览器中通常会把DOM和JavaScript独立实现,两个相互独立的功能只要通过接口彼此连接,就会产生消耗。

DOM访问与修改

对比一下两个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
	function innerHTMLLoop1 () {
	    for (var count = 0; count < 15000; count++) {
	        document.getElementById('here').innerHTML += 'a';
	    }
	}
	
	function innerHTMLLoop2 () {
	    var content = '';
	    for (var count = 0; count < 15000; count++) {
	        content += 'a';
	    }
	    document.getElementById('here').innerHTML += content;
	}
	

第一个实例中多次访问修改DOM,第二个实例中则在javascript中循环处理好内容之后,一次性访问修改DOM.所以:减少访问DOM的次数,把运算尽量留在ECMAScript这一端处理

innerHTML与原生DOM方法对比实验表明,在旧版浏览器中innerHTML的优势较明显,但在新版本中则不那么明显,在基于webkit内核的新版本浏览器中DOM方法反而略胜一筹。

使用DOM方法更新页面内容的另一个途径是克隆已有元素,即使用 element.cloneNode() 替代 document.createElement() ,效率会稍有提高。

HTML Collections HTML集合 *

HTML集合是包含了DOM节点引用的类数组对象,以下方法的返回值就是一个集合:

下面的属性同样返回HTML集合:

以上返回的都是HTML集合对象,是个类似数组的列表。它们并不是真的数组,但提供了一个类似数组中的length属性,而且可以以数字索引的方式访问列表里的元素。其中一个重要的特性:

正如DOM标准中所定义的,HTML集处于一种“实时状态”实时存在,这意味着当底层文档对象更新时,它也会自动更新(参考)。

这就意味着集合是一直和文档保持连接的,每次需要更新信息时,都会重复查询文档,哪怕只是获取它的length属性。这就是它低效的原因。

下面是一个有趣的死循环:

1
2
3
4
5
6
	// 一个意外的死循环
	var alldivs = document.getElementsByTagName('div');
	for (var i = 0; i < alldivs.length; i++) {
	    document.body.appendChild(document.createElement('div'));
	}
	

代码中的alldivs.length反映出的底层文档的当前状态。这样的遍历操作不仅可能会导致逻辑错误,而且很慢,每次迭代都要执行查询操作。

优化方法有两种,一种是讲HTML集合拷贝到普通数组中:

1
2
3
4
5
6
7
	function toArray (collection) {
	    for (var i = 0, a = [], len = collection.length; i < len; i++) {
	        a[i] = collection[i];
	    }
	    return a;
	}
	

另一种方法是将collection的长度缓存到局部变量中。一般来说遍历较小的集合加入了缓存就可以了。在相同的内容和数量下,遍历一个数组的速度是明显快于遍历一个HTML集合的。但是将集合放到数组中又需要额外的一次遍历,所以应该进行衡量评估,再选用合适的方法。

访问集合元素的时候也同样可以使用局部变量来缓存此成员,然后用局部变量去访问元素,下面是三个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
	// 较慢
	function collectionGlobal () {
	    var collection = document.getElementsByTagName('div'),
	        len = collection.length,
	        name = '';
	    for (var count = 0; count < len; count++) {
	        name = document.getElementsByTagName('div')[count].nodeName;
	        name = document.getElementsByTagName('div')[count].nodeType;
	        name = document.getElementsByTagName('div')[count].tagName;
	    }
	    return name;
	}
	
	// 较快
	function collectionLocal () {
	    var collection = document.getElementsByTagName('div'),
	        len = collection.length,
	        name = '';
	    for (var count = 0; count < len; count++) {
	        name = collection[count].nodeName;
	        name = collection[count].nodeType;
	        name = collection[count].tagName;
	    }
	    return name;
	}
	
	// 最快
	function collectionNodesLocal () {
	    var collection = document.getElementsByTagName('div'),
	        len = collection.length,
	        name = '',
	        el = null;
	    for (var count = 0; count < len; count++) {
	        el = collection[count];el
	        name = el.nodeName;
	        name = el.nodeType;
	        name = el.tagName;
	    }
	    return name;
	}
	

遍历DOM

1
2
3
4
5
6
7
8
	
			// 返回值是一个NodeList:包含着匹配节点的类数组对象,
			// 区别于HTML集合,它并不会对应实时的文档结构
			var elements = document.querySelectorAll('#menu a');
			
			// 返回值是HTML集合
			var elements = document.getElementById('menu').getElementsByTagName('a');
			
	如果要处理大量组合查询,使用`querySelectorAll()`会更有效率,以下例子是很好的对比:
1
2
3
4
5
6
7
8
9
10
11
12
			var errs = document.querySelectorAll('div.warning, div.notice');
			
			var errs = [],
				divs = document.getElementsByTagName('div'),
				classname = '';
			for (var i = 0, len = divs.length; i < len; i++) {
				classname = divs[i].className;
				if (classname === 'notice' || classname === 'warning') {
					errs.push(divs[i]);
				}
			}
			
	第一段代码出了更简洁之外,效率还比第二段高2~6倍。
	
*	支持的浏览器: IE8, Firefox 3.5+, Safari 3.1+, Chrome1+, Opera 10+

###重绘与重排

浏览器下载完成页面中的所有组件:HTML标记,JavaScript,CSS,图片,之后会解析并生成两个内部数据结构:

当DOM的变化影响了元素的集合属性(宽高),浏览器需要重新计算元素的集合属性,同样其他元素的集合属性和位置也会因此受到影响。浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树,这个过程称为“重排(reflow)”;完成重排后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为“重绘(repaint)”。

重绘和重排操作都是代价昂高的操作,导致UI反应迟钝,要尽量减少。

####重排何时发生?

页面布局和几何属性发生改变:

####渲染树变化的排列与刷新

获取布局信息的操作会导致队列刷新

在修改样式的过程中,最好避免使用上面列出的属性,因为不管它本身有没有改变,使用以上属性都会刷新渲染队列。*

####最小化重绘和重排

优化方法:合并多次对DOM和样式的修改,然后一次性处理掉。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
	// 可能触发多次重排
	var el = document.getElementById('myDiv');
	el.style.borderLeft = '1px';
	el.style.borderRight = '2px';
	el.style.padding = '5px';
	
	// 优化
	el.style.cssText += 'border-left: 1px; border-right:2px; padding: 5px;';
	
	// 另一种优化, 将样式写到CSS的class中
	el.className = 'active';
	

要对DOM元素进行一系列操作时,可通过一下步骤来减少重绘和重排的次数:

  1. 使元素脱离文档流
  2. 对其应用多重改变
  3. 把元素带回文档中

其中第1,3步各触发一次重排。

三种基本方法可以使DOM脱离文档:

####缓存布局信息

查询布局信息,例如: offsets,scroll values或computedstyle values的时候,浏览器为了返回最新的值,会刷新队列并应用所有变更。

优化犯法:用局部变量缓存布局信息,减少布局信息的获取次数

考虑一下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	// 低效的
	myElement.style.left = 1 + myElement.offsetLeft + 'px';
	myElement.style.top = 1 + myElement.offsetTop + 'px';
	if (myElement.offsetLeft >= 500) {
	    stopAnimation();
	}
	
	// 优化: 缓存offsets值
	var current = myElement.offsetLeft;
	
	//...
	
	current++;
	myElement.style.left = current + 'px';
	myElement.style.top = current + 'px';
	if (current >= 500) {
	    stopAnimation();
	}
	

####让元素脱离动画流

动画中使用绝对定位,使用拖放代理

####IE和hover

在IE中如果大量元素使用了:hover,会降低响应速度。IE8尤其明显。

####事件委托

使用事件委托来减少事件处理器的数量。

原理:事件逐层冒泡并能被父级元素捕获

参考以下gist:

其中要注意跨浏览器的部分,包括: