JS性能优化——DOM编程
浏览器中的DOM 天生就慢
DOM是个与语言无关的API,它在浏览器中的接口却是用JavaScript实现的。客户端脚本编程大多数时候是在个底层文档打交道,DOM就成为现在JavaScript编码中的重要部分。
DOM访问和修改
ECMAScript 每次访问DOM 都会产生性能损耗。
修改元素则更为昂贵,因为它会导致浏览器重新计算页面的几何变换。
最坏的情况是在循环中访问或修改元素,尤其是对HTML元素集合循环操作。
function innerHtmlLoop(){ for(var count = 0; count <15000; count++){ document.getElementById('here').innerHTML +="a"; } }
这个函数循环修改页面元素的内容,每次循环迭代,该元素都被访问两次:一次读取innerHtml的属性值,另一次重写它。
换一种效率更高的方法,用局部变量存储修改中的内容,再循环结束后一次性写入:
function innerHtmlLoop(){ var content = ''; for(var count = 0; count <15000; count++){ count += 'a'; } document.getElementById('here').innerHTML += content; }
这种方式比上边的快了155倍。
访问DOM的次数越多,代码的运行速度越慢。因此,通用的经验法则是:减少访问DOM的次数,把运算尽量留在ECMAScript这一端处理。
innerHTML对比DOM方法:推荐使用innerHTML 而不是原生DOM方法生成HTML,绝大部分浏览器中都是innerHTML运行的更快。但是对于大多数日常的操作而言,并没有太大的区别,所以根据可读性、稳定性、团队习惯、代码风格来综合决定使用哪种方式。
节点克隆:element.cloneNode()(element表示已有节点)替代document.createElement()。 在大多数浏览器中节点克隆更有效率,但是也不是特别明显。
HTML集合:是包含了DOM节点引用的类数组对象,eg:document.getElementsByName();...或者:document.images页面中所有的img元素document.links所有a元素...
返回值为HTML集合对象,是个类数组的列表。但是并不是真正的数组(因为没有slice和push之类的方法),但是提供了一个类似数组中的length的属性,并且还能以数字索引的方式访问列表中的元素。
遍历这种类数组的集合,读取元素集合的length属性会引发集合进行更新,这在所有的浏览器中都有明显的性能问题,优化方法:将集合的长度缓存到循环外的局部变量中,然后在循环的条件退出语句中使用该变量:
function loopCacheLengthCollection(){ var coll = document.getElementsByTagName('div'); //这里的coll是集合 类数组 len = coll.length; //将集合的长度缓存到局部变量len中 for(var count = 0; count < len; count++){ //不要在这里写 count < coll.length,会明显影响性能 ,
//如果coll是数组,那么 count < coll.length 对性能影响并不大 /* 代码处理 */ } }
遍历DOM:可以使用document.querySelector('.myclass')的方法来查询整个文档,活通过elref.querySelector('.myclass')在子树中进行查询,这里的elref是一个DOM元素的引用。
重绘与重排
浏览器在下载完页面中的所有组件---HTML标记、JavaScript、css、图片,之后会解析并生成两个内部数据结构:
DOM树:表示页面结构
渲染树:表示DOM节点如何显示
重排何时发生:
添加或删除可见的DOM元素;
元素位置改变;
元素尺寸改变(内外边距,边框厚度,宽高等);
内容改变;文本改变或者图片被另一个不同尺寸的图片替代
页面渲染器初始化;
浏览器窗口尺寸改变
渲染树变化的排队与刷新:
offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop, scrollLeft, scrollWidth, scrollHeight
clientTop, clientLeft, clientWidth, clientHeight
getComputedStyle()(currentStyle in IE)
这些方法需要返回最新的布局信息,因此浏览器不得不执行渲染队列中的“待处理变化”并处罚重排以返回正确的值,在修改样式的过程中,最好避免使用上边列出的属性
var computed, tmp = '', bodystyle = document.body.style;
if(document.body.currentStyle){ computed = document.body.currentStyle; } else{ computed = document.defaultView.getComputedStyle(document.body,''); } //修改同一属性低效的方式 //然后获取样式信息 bodystyle.color = 'red'; tmp = computed.backgroundColor; bodystyle.color = 'white'; tmp = computed.backgroundImage; bodystyle.color = 'green'; tmp = computed.backgroundAttachment; //每次修改够都读取一个computed样式属性。读取的属性backgroundColor、backgroundImage、backgroundAttachment都与改变的颜色无关。然而浏览器却需要刷新渲染队列并重排,因为compited的样式属性被请求了。 //更有效的方法,性能更快。如下: bodystyle.color = 'red'; bodystyle.color = 'white'; bodystyle.color = 'green'; tmp = computed.backgroundColor; tmp = computed.backgroundImage; tmp = computed.backgroundAttachment;
最小化重绘和重排:重绘和重排可能代价非常昂贵,因此减少此类操作的发生。可以合并多次对DOM和样式的修改,然后依次处理掉。
var el = document.getElementById('mydiv'); el.style.borderLeft = '1px'; el.style.borderRight = '2px'; el.style.padding = '5px'; //最糟糕情况下回导致浏览器触发三次重排。大部分浏览器为此做了优化,只会触发一次重排,但是如果在上边代码执行时,有其他代码请求布局信息,那么就会导致三次重排 //而且这段代码四次请求DOM,可以被优化: var el = document.getElementById('mydiv'); el.style.cssText = 'boeder-left: 1px; border-right: 2px; padding: 5px;';//如果不想覆盖原有的样式 可以写 el.style.cssText += ';boeder-left: 1px;';
//修改css的class名称,更易于维护,可能会带来轻微的性能问题,因为改变类时需要检查级联样式。
var el = document.getElementById('mydiv');
el.className = 'active';
批量修改DOM:当你对DOM元素进行一系列操作时,可以通过下边的步骤来减少重绘和重排:
1、使元素脱离文档流
2、对其应用多重改变
3、把元素带回文档中。
该过程会触发两次重排(①和③)。但是如果你忽略这两个步骤,那么在第二步所产生的任何修改都会触发一次重排
使DOM脱离文档的三种基本方法:
①隐藏元素,应用修改,重新显示
//为了演示脱离文档的操作,考虑下边的链接列表,它必须更新更多的信息 <ul id = "mylist"> <li><a href = "http://phpied.com">Stoyan</a></li> <li><a href = "http://julienlecomte.com">Stoyan</a></li> </ul> //假设附加数据已经存储在一个对象中,并要插入列表。这些数据定义如下: var data = [ { "name": "Nicholas", "url": "http://nczonline.net" }, { "name": "Ross", "url": "http://techfoolery.com" } ]; //下面是一个用来更新指定节点数据的通用函数: function appendDataToElement(appendToElement, data){ var a, li; for(var i = 0; max = data.length; i++){ a = document.createElement('a'); a.href = data[i].url; a.appendChild(document.createTextNode(data[i].name)); li = document.createElement('li'); li.appendChild(a); appendToElement.appendChild(li); } } //最明显的方法: var ul = document.gerElementById('mylist'); appendDataToElement(ul, data); //但是这种方法,data的每一个新条目被附加到当前DOM树时都会导致重排。 //第一种方法,改变display属性,临时从文档中移除<ul>元素,然后再回复它: var ul = document.getElementById('mylist'); ul.style.display = 'none'; appendDataToElement(ul, data); ul.style.display = 'block';
②使用文档片段在当前DOM之外构建一个子树,再把它拷贝回文档(推荐使用)
var fragment = document.createDocumentFragment(); appendDataToElement(fragment,data); docuemnt.getElementById(mylist').appendChild(fragment);
③将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素
var old = document.getElementById('mylist'); var clone = old.cloneNode(true); appendDataToElement(old,data); old.parentNode.replaceChild(clone,old);
缓存布局信息:
myElement.style.left = 1 + myElement.offsetLeft + 'px';
---> current++; myElement.style.left = 1 + current + 'px';
让元素脱离动画流:
一般情况,重排只影响渲染树中的一小部分,但也可能影响很大的部分,甚至整个渲染树。浏览器所需要重排的次数越少,应用程序的响应速度就越快。
因此当页面的一个动画推移页面整个余下的部分时,会导致一次代价昂贵的大规模重排,用户会感到页面一顿一顿的。渲染树中需要重新计算的节点越多,情况就会越糟。
拒绝重排:1、使用绝对位置定位页面上的动画元素,将其脱离文档流。
2、让元素动起来。当它扩大时,会临时覆盖部分页面,但这只是页面一个小区域的重绘过程,不会产生重排并重绘页面的大部分内容。
3、当动画结束时恢复定位,从而只会下移一次文档的其他元素。
IE和:hover:
从IE7开始,IE允许在任何元素(严格模式)上使用 :hover 这个css伪选择器。但是如果大量使用 :hover,那么会降低响应速度。这个问题在IE8中更为明显。
很大的表格或很长的列表,应避免使用这种效果。
事件委托
当页面中存在大量元素,而且每一个都要一次或者多次绑定事件处理器时,这种情况可能会影响性能。每绑定一个事件处理器都是有代价的 。需要访问和修改的DOM元素越多,应用程序也就越慢,特别是时间绑定通常发生在onload(或DOMContentReady)时,此时对每一个富交互应用的网页来说都是一个拥堵的时刻。事件绑定占用了处理的时间,而且,浏览器需要跟踪每个事件处理器,这也会占用更多的内存。当这些工作结束时,这些事件处理器中的绝大部分都不再需要(因为并不是100%的按钮或链接会被用户点击),因此有很多工作是没有必要的。
事件委托可以很好的处理这类问题。原理:事件逐层冒泡并能被父级元素捕获。使用事件代理,只需要给外层元素绑定一个处理器,就可以处理在其子元素上触发的所有事件。
1、访问事件对象,并判断事件源
2、取消文档树中的冒泡(可选)
3、阻止默认动作(可选)
小结:
最小化DOM访问次数,尽可能在JavaScript端处理。
如果需要多次访问某个DOM节点,请使用局部变量存储它的引用
小心处理HTML集合,因为它实时连系着底层文档,把集合的长度缓存到一个变量中,并在迭代中使用它。如果需要经常操作集合,建议把它拷贝到一个数组中。
如果可能的话,使用速度更快的API,比如querySelectorAll()和firstElementChild。
要留意重绘和重排;批量修改样式时,“离线”操作DOM树,使用缓存,并减少访问布局信息的次数
动画中使用绝对定位,使用拖放代理
使用事件委托来减少事件处理器的数量