文档对象模型(DOM)是一个独立 于特定语言的应用程序接口。在浏览器中,DOM接口是以JavaScript语言实现的,经过JavaScript来操做浏览器页面中的元素,这使得 DOM成为了JavaScript中重要的组成部分。在富客户端网页应用中,界面上UI的更改都是经过DOM操做实现的,并非经过传统的刷新页面实现 的。尽管DOM提供了丰富接口供外部调用,但DOM操做的代价很高,页面前端代码的性能瓶颈也大多集中在DOM操做上,因此前端性能优化的一个主要的关注 点就是DOM操做的优化。DOM操做优化的总原则是尽可能减小DOM操做。javascript
在讨论DOM操做的最佳性能实践以前,先来看看DOM操做为何会影响性能。在浏览器中,DOM的实现和ECMAScript的实现是分离的。好比 在IE中,ECMAScrit的实如今jscript.dll中,而DOM的实如今mshtml.dll中;在Chrome中使用WebKit中的 WebCore处理DOM和渲染,但ECMAScript是在V8引擎中实现的,其余浏览器的状况相似。因此经过JavaScript代码调用DOM接 口,至关于两个独立模块的交互。相比较在同一模块中的调用,这种跨模块的调用其性能损耗是很高的。但DOM操做对性能影响最大其实仍是由于它致使了浏览器 的重绘(repaint)和重排(reflow)。css
为了让你们能更深入地理解重绘和重排对性能的影响,这里须要简单叙述一下浏览器的渲染原理(若是想详细了解浏览器的工做原理,请参照文章《浏览器的工做原理:新式网络浏览器幕后揭秘》 )。从下载文档到渲染页面的过程当中,浏览器会经过解析HTML文档来构建DOM树,解析CSS产生CSS规则树。JavaScript代码在解析过程当中, 可能会修改生成的DOM树和CSS规则树。以后根据DOM树和CSS规则树构建渲染树,在这个过程当中CSS会根据选择器匹配HTML元素。渲染树包括了每 个元素的大小、边距等样式属性,渲染树中不包含隐藏元素及head元素等不可见元素。最后浏览器根据元素的坐标和大小来计算每一个元素的位置,并绘制这些元 素到页面上。重绘指的是页面的某些部分要从新绘制,好比颜色或背景色的修改,元素的位置和尺寸并没用改变;重排则是元素的位置或尺寸发生了改变,浏览器需 要从新计算渲染树,致使渲染树的一部分或所有发生变化。渲染树从新创建后,浏览器会从新绘制页面上受影响的元素。重排的代价比重绘的代价高不少,重绘会影 响部分的元素,而重排则有可能影响所有的元素。以下的这些DOM操做会致使重绘或重排:html
增长、删除和修改可见DOM元素前端
页面初始化的渲染html5
移动DOM元素java
修改CSS样式,改变DOM元素的尺寸node
DOM元素内容改变,使得尺寸被撑大浏览器
浏览器窗口尺寸改变缓存
浏览器窗口滚动性能优化
能够看出,这些操做都是DOM操做中比较常见的。现代浏览器会针对重排或重绘作性能优化,好比,把DOM操做积累一批后统一作一次重排或重绘。但在有些状况下,浏览器会当即重排或重绘。好比请求以下的DOM元素布局信息:offsetTop/Left/Width/Height
、scrollTop/Left/Width/Height
、clientTop/Left/Width/Height
、getComputedStyle()
或 currentStyle
。由于这些值都是动态计算的,因此浏览器须要尽快完成页面的绘制,而后计算返回值,从而打乱了重排或重绘的优化。
DOM操做带来的页面重绘或重排是不可避免的,但能够遵循一些最佳实践来下降因为重排或重绘带来的影响。以下是一些具体的实践方法:
1. 合并屡次的DOM操做为单次的DOM操做
最多见频繁进行DOM操做的是频繁修改DOM元素的样式,代码相似以下:
element.style.borderColor = '#f00';
element.style.borderStyle = 'solid';
element.style.borderWidth = '1px';
这种编码方式会由于频繁更改DOM元素的样式,触发页面屡次的重排或重绘,上面介绍过,现代浏览器针对这种状况有性能的优化,它会合并DOM操做,但并非全部的浏览器都存在这样的优化。推荐的方式是把DOM操做尽可能合并,如上的代码能够优化为:
// 优化方案1
element.style.cssText += 'border: 1px solid #f00;';
// 优化方案2
element.className += 'empty';
示例的代码有两种优化的方案,都作到了把屡次的样式设置合并为一次设置。方案2比方案1稍微有一些性能上的损耗,由于它须要查询CSS类。但方案2的维护性最好,这在上一章曾经讨论过。不少时候,若是性能问题并不突出,选择编码方案时须要优先考虑的是代码的维护性。
相似的操做还有经过innerHTML接口修改DOM元素的内容。不要直接经过此接口来拼接HTML代码,而是以字符串方式拼接好代码后,一次性赋值给DOM元素的innerHTML
接口。
2. 把DOM元素离线或隐藏后修改
把DOM元素从页面流中脱离或隐藏,这样处理后,只会在DOM元素脱离和添加时,或者是隐藏和显示时才会形成页面的重绘或重排,对脱离了页面布局流的DOM元素操做就不会致使页面的性能问题。这种方式适合那些须要大批量修改DOM元素的状况。具体的方式主要有三种:
(1)使用文档片断
文档片断是一个轻量级的document对象,并不会和特定的页面关联。经过在文档片断上进行DOM操做,能够下降DOM操做对页面性能的影响,这 种方式是建立一个文档片断,并在此片断上进行必要的DOM操做,操做完成后将它附加在页面中。对页面性能的影响只存在于最后把文档片断附加到页面的这一步 操做上。代码相似以下:
var fragment = document.createDocumentFragment();
// 一些基于fragment的大量DOM操做
...
document.getElementById('myElement').appendChild(fragment);
(2)经过设置DOM元素的display样式为none来隐藏元素
这种方式是经过隐藏页面的DOM元素,达到在页面中移除元素的效果,通过大量的DOM操做后恢复元素原来的display样式。对于这类会引发页面重绘或重排的操做,就只有隐藏和显示DOM元素这两个步骤了。代码相似以下:
var myElement = document.getElementById('myElement');
myElement.style.display = 'none';
// 一些基于myElement的大量DOM操做
...
myElement.style.display = 'block';
(3)克隆DOM元素到内存中
这种方式是把页面上的DOM元素克隆一份到内存中,而后再在内存中操做克隆的元素,操做完成后使用此克隆元素替换页面中原来的DOM元素。这样一来,影响性能的操做就只是最后替换元素的这一步操做了,在内存中操做克隆元素不会引发页面上的性能损耗。代码相似以下:
var old = document.getElementById('myElement');
var clone = old.cloneNode(true);
// 一些基于clone的大量DOM操做
...
old.parentNode.replaceChild(clone, old);
在现代的浏览器中,由于有了DOM操做的优化,因此应用如上的方式后可能并不能明显感觉到性能的改善。可是在仍然占有市场的一些旧浏览器中,应用以上这三种编码方式则能够大幅提升页面渲染性能。
3. 设置具备动画效果的DOM元素的position属性为fixed或absolute
把页面中具备动画效果的元素设置为绝对定位,使得元素脱离页面布局流,从而避免了页面频繁的重排,只涉及动画元素自身的重排了。这种作法能够提升动 画效果的展现性能。若是把动画元素设置为绝对定位并不符合设计的要求,则能够在动画开始时将其设置为绝对定位,等动画结束后恢复原始的定位设置。在不少的 网站中,页面的顶部会有大幅的广告展现,通常会动画展开和折叠显示。若是不作性能的优化,这个效果的性能损耗是很明显的。使用这里提到的优化方案,则能够 提升性能。
4. 谨慎取得DOM元素的布局信息
前面讨论过,获取DOM的布局信息会有性能的损耗,因此若是存在重复调用,最佳的作法是尽可能把这些值缓存在局部变量中。考虑以下的一个示例:
for (var i=0; i < len; i++) {
myElements[i].style.top = targetElement.offsetTop + i*5 + 'px';
}
如上的代码中,会在一个循环中反复取得一个元素的offsetTop
值,事实上,在此代码中该元素的offsetTop
值并不会变动,因此会存在没必要要的性能损耗。优化的方案是在循环外部取得元素的offsetTop值,相比较以前的方案,此方案只是调用了一遍元素的offsetTop
值。更改后的代码以下:
var targetTop = targetElement.offsetTop;
for (var i=0; i < len; i++) {
myElements[i].style.top = targetTop+ i*5 + 'px';
}
另外,由于取得DOM元素的布局信息会强制浏览器刷新渲染树,而且可能会致使页面的重绘或重排,因此在有大批量DOM操做时,应避免获取DOM元素 的布局信息,使得浏览器针对大批量DOM操做的优化不被破坏。若是须要这些布局信息,最好是在DOM操做以前就取得。考虑以下一个示例:
var newWidth = div1.offsetWidth + 10;
div1.style.width = newWidth + 'px';
var newHeight = myElement.offsetHeight + 10; // 强制页面重排
myElement.style.height = newHeight + 'px'; // 又会重排一次
根据上面的介绍,代码在遇到取得DOM元素的信息时会触发页面从新计算渲染树,因此如上的代码会致使页面重排两次,若是把取得DOM元素的布局信息提早,由于浏览器会优化连续的DOM操做,因此实际上只会有一次的页面重排出现,优化后的代码以下:
var newWidth = div1.offsetWidth + 10;
var newHeight = myElement.offsetHeight + 10;
div1.style.width = newWidth + 'px';
myElement.style.height = newHeight + 'px';
5. 使用事件托管方式绑定事件
在DOM元素上绑定事件会影响页面的性能,一方面,绑定事件自己会占用处理时间,另外一方面,浏览器保存事件绑定,因此绑定事件也会占用内存。页面中 元素绑定的事件越多,占用的处理时间和内存就越大,性能也就相对越差,因此在页面中绑定的事件越少越好。一个优雅的手段是使用事件托管方式,即利用事件冒 泡机制,只在父元素上绑定事件处理,用于处理全部子元素的事件,在事件处理函数中根据传入的参数判断事件源元素,针对不一样的源元素作不一样的处理。这样就不 须要给每一个子元素都绑定事件了,管理的事件绑定数量变少了,天然性能也就提升了。这种方式也有很大的灵活性,能够很方便地添加或删除子元素,不须要考虑因 元素移除或改动而须要修改事件绑定。示例代码以下:
// 获取父节点,并添加一个click事件
document.getElementById('list').addEventListener("click",function(e) { // 检查事件源元素 if(e.target && e.target.nodeName.toUpperCase == "LI") { // 针对子元素的处理 ...
}
});
上述代码中,只在父元素上绑定了click事件,当点击子节点时,click事件会冒泡,父节点捕获事件后经过e.target检查事件源元素并作相应地处理。
在JavaScript中,事件绑定方式存在浏览器兼容问题,因此在不少框架中也提供了类似的接口方法用于事件托管。好比在jQuery中可使用以下方式实现事件的托管(示例代码来自jQuery官方网站):
$( "table" ).on( "click", "td", function() { $( this ).toggleClass( "chosen" );
});