一.Dom 的性能瓶颈及原因
1. 为什么是 DOM
标准的 xml/html 的文本解析协议,常见的有 DOM 与 SAX。在解析速度及内存占用上,SAX 比 DOM 有优势,但为什么浏览器选择 DOM 解析 html?
(1)DOM VS SAX
SAX 提供一次性解析文本,不生成对象,Iterator 模式访问元素,event-based,PUSH 模式触发,简单说:App 需要向 Parser 注册,当 Parser 遍历 xml 时,触发调用 APP 。想深入体验,用下 javax.xml.parsers.SAXParser。这里说个题外话,改进版 StAX 是 PULL 模式,但这都不重要了,重要是:一次性文本解析,不生成对象。
DOM 解析文本后,生成 DOM 树。即:一次性文本解析,生成对象。
(2)浏览器选择了 DOM
单次效率 DOM 不如 SAX,但 SAX 不生成对象,浏览器很多操作很难满足,比如:元素定位,元素样式渲染……所以 DOM 是必然之选。
2. DOM 的性能问题
【1】核心问题
当解析的 html 文件很大时,生成 DOM 树占用内存较大,同时遍历(不更新)元素耗时也更长。但这都不是重点,DOM 的核心问题是:DOM 修改导致的页面重绘、重新排版!重新排版是用户阻塞的操作,同时,如果频繁重排,CPU 使用率也会猛涨!
DOM 操作会导致一系列的重绘(repaint)、重新排版(reflow)操作。为了确保执行结果的准确性,所有的修改操作是按顺序同步执行的。大部分浏览器都不会在 JavaScript 的执行过程中更新 DOM。相应的,这些浏览器将对对 DOM 的操作放进一个队列,并在 JavaScript 脚本执行完毕以后按顺序一次执行完毕。也就是说,在 JavaScript 执行的过程,直到发生重新排版,用户一直被阻塞。
一般的浏览器中(不含 IE),repaint 的速度远快于 reflow,所以避免 reflow 更重要。
导致 repaint、reflow 的操作:
* DOM 元素的添加、修改(内容)、删除( Reflow + Repaint)
* 仅修改 DOM 元素的字体颜色(只有 Repaint,因为不需要调整布局)
* 应用新的样式或者修改任何影响元素外观的属性
* Resize 浏览器窗口、滚动页面
* 读取元素的某些属性(offsetLeft、offsetTop、offsetHeight、offsetWidth、scrollTop/Left/Width/Height、clientTop/Left/Width/Height、getComputedStyle()、currentStyle(in IE))
【2】其他
某些 Javascript 框架中,CSS 选择器,如:var el = $('.hyddd');由于 IE6、7 不支持,所以 Javascript 框架必须通过遍历整个 DOM 树来寻找对象。
二. 针对 DOM 问题,Javascript 的应对方案
1. 针对 repaint、reflow,Nicholas 大叔在他的《Speed up your JavaScript, Part 4》做了详细介绍,这里我也整理一下:
解决问题的关键是:减少因 DOM 操作,引起的 reflow。Nicholas 总结了一些方法:
【1】在 DOM 外,执行尽量多的变更操作。Demo:
// 不好的做法 for (var i=0; i < items.length; i++){ var item = document.createElement("li"); item.appendChild(document.createTextNode("Option " + i); list.appendChild(item); }
// 更好的做法 // 使用容器存放临时变更, 最后再一次性更新 DOM 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);
【2】操作 DOM 前,先把 DOM 节点删除或隐藏,因为隐藏的节点不会触发重排。Demo 如下:
list.style.display = "none"; for (var i=0; i < items.length; i++){ var item = document.createElement("li"); item.appendChild(document.createTextNode("Option " + i); list.appendChild(item); } list.style.display = "";
【3】一次性,修改样式属性。Demo 如下:
// 不好的做法 // 这种做法会触发多次重排 element.style.backgroundColor = "blue"; element.style.color = "red"; element.style.fontSize = "12em";
// 更好的做法是,把样式都放在一个 class 下 .newStyle { background-color: blue; color: red; font-size: 12em; } element.className = "newStyle";
【4】使用缓存,缓存临时节点。
// 不好的做法 document.getElementById("myDiv").style.left = document.getElementById("myDiv").offsetLeft + document.getElementById("myDiv").offsetWidth + "px";
// 更好的做法 var myDiv = document.getElementById("myDiv"); myDiv.style.left = myDiv.offsetLeft + myDiv.offsetWidth + "px";
2. 针选择其的问题,我只能说:没办法…… : (
三. 参考资源
2. 《Speed up your JavaScript, Part 4》
现在的 web 应用越来越复杂,需要响应各种各样的用户触发事件,因而也就不可避免的,需要给我们的 html 页面上的 dom 元素增加事件监听函数。
我们知道给 dom 元素绑定事件监听函数的方法有如下 3 种:
1. 页面 html:
2. 页面 html:
Javascript:
3. 页面 html:
Javascript:
这 3 种方法的功能效果和差异,大家都了解,在此就不在赘述了。但是这 3 种方法,对页面渲染的速度,资源的消耗,却是有很大不同的。
正文后面的 html 代码是一个 demo 页面,大家可以用 ie 浏览器打开,通过注释不同的代码段,查看页面运行效果。
可以看到第一种方式的效率是最低的,随着页面节点的增多,页面渲染时间急剧增加,在 ie7 下运行,大概 670ms。
第二种方式明显好一些,在 ie7 下,大概 250ms。
而第三种方式则是最快的方法,也是 web 前端开发推荐的标准写法,在 ie7 下,大概 188ms。
然后我们去掉事件绑定的逻辑,发现只渲染 dom 元素,不绑定事件的时间,仅仅 125ms。可见事件绑定的时间消耗还是很大的,尤其是第一种方式,也就是 Dom Level 0 Event,最为耗时。
另外,大家运行各段代码的时候,不妨打开任务管理器,找到浏览器对应的进程,查看代码运行时 cpu 的消耗以及内存的使用。
我们可以看到,Dom Level 0 Event,对 cpu 的消耗明显要高很多。
对内存的消耗分析:
重新打开浏览器,空白页面的内存占用量大概是 37M,虚拟内存为 28M,页面渲染后:
1. 内存使用 54M,虚拟内存 41M;
2. 内存使用 44M,虚拟内存 31M;
3. 内存使用 44M,虚拟内存 31M。
可见 Dom Level 0 Event 对内存的消耗,也远远超出了其它方式。
为什么 Dom Level 0 Event 会这么消耗系统资源呢?对 cpu 和内存的消耗都远远超出了其它方式,我们来做一个简单分析。
为了便于分析,我们不妨修改一下我们的代码 <button onclick=”debugger;test();”></button> ,然后运行页面,在 ie 的 script debugger 里我们找到堆栈调用这一项,可以看到有一个 anonymos function,这个 function 是从何而来的呢?原来浏览器在对 Dom Level 0 Event 做绑定的时候,会自动生成一个包含我们的代码的匿名函数,然后把这个匿名函数绑定到事件,类似于如下方式:
test();
} ;
而 ie 浏览器又没有足够的智能,区分出众多内部功能完全一致的匿名函数,并合并它们的引用,所以导致了随着 dom 事件绑定的越来越多,匿名函数的个数也越来越多。因为要声明数量众多的事件处理匿名函数,也就不难明白,为什么会消耗如此多的系统资源了。
随着 dom 元素的增多,这个资源消耗就会越来越严重。而且我们可以尝试着刷新一下页面,发现随着刷新的次数增加,页面运行越来越慢,cpu 消耗也越来越多,内存也会有少量增加。可见,Dom Level 0 Event 还会带来少量的内存泄露,至于时间的延长,cpu 消耗的加剧,推测是因为浏览器忙于释放众多的匿名函数所占用的资源所带来的后果。
进一步深入,由于 ie 浏览器是基于冒泡的事件模型,子元素的 event 会冒泡到父元素,所以更极致的优化是去掉众多子元素的事件绑定,而将事件绑定到父元素。在正文后的 demo 中,也有这方面的尝试,可以看到不仅 cpu,内存消耗最低,时间上也跟渲染干净的 html 页面是一样的。
所以,我们在页面事件绑定中,要尽量避免 Dom Level 0 Event,而且要尽可能的将事件上升(当然也要考虑事件处理的灵活性)。
demo:
<ul id="list"></ul>
<SCRIPT LANGUAGE="JavaScript">
<!--
var $ = function(id){
return document.getElementById(id)
};
function test(){
alert(1)
}
var ul = $("list");
var count = 5000;
// ie7
//-->
</SCRIPT>
<script>
var d = new Date()
var str = [];
for(var i = 0;i<count;i++){
str.push('<li onclick="test();">'+i+'</li>')
}
ul.innerHTML = str.join("");
alert(new Date - d);
//670 刷新时时间增加 85
</script>
<SCRIPT LANGUAGE="JavaScript">
<!--
/*var d = new Date()
var str = [];
for(var i = 0;i<count;i++){
str.push('<li>'+i+'</li>')
}
ul.innerHTML = str.join("");
alert(new Date - d); */
//125
//-->
</SCRIPT>
<SCRIPT LANGUAGE="JavaScript">
<!--
/*var d = new Date()
var str = [];
for(var i = 0;i<count;i++){
str.push('<li>'+i+'</li>')
}
ul.innerHTML = str.join("");
var li = document.getElementsByTagName("li");
var l = li.length;
for(var i=0;i<l;i++){
li[i].onclick = test;
}
li = null;
alert(new Date - d);*/
//250
//-->
</SCRIPT>
<SCRIPT LANGUAGE="JavaScript">
<!--
/*var d = new Date()
var str = [];
for(var i = 0;i<count;i++){
str.push('<li>'+i+'</li>')
}
ul.innerHTML = str.join("");
var li = document.getElementsByTagName("li");
var l = li.length;
for(var i=0;i<l;i++){
li[i].attachEvent("onclick",test);
}
li = null;
alert(new Date - d);*/
//188
//-->
</SCRIPT>
<SCRIPT LANGUAGE="JavaScript">
<!--
/*var d = new Date()
var str = [];
for(var i = 0;i<count;i++){
str.push('<li>'+i+'</li>')
}
ul.innerHTML = str.join("");
ul.attachEvent("onclick",test);
alert(new Date - d);*/
//125
//-->
</SCRIPT>
</BODY>
今天办公室里几个男女在讨论用避孕套的好处和坏处,男方坚决抵制避孕套,女方大力支持使用避孕套,双方各持一词不分伯仲,这时我们办公室一个大姐问我平日用套套吗?我说:我……我...我都是包皮打个结。当时讨论就结束了。