本文是阅读《高性能JavaScript》一书后,从 DOM编程 模块对JavaScript性能优化做了部分总结,记录一下。可能总结的不好,不是很完整,也希望各位大佬能多给出一些建议。万分感谢!
慢的必然性
- 简单理解,两个相互独立的功能只要通过接口彼此连接,就会产生消耗。
DOM 的修改与访问
访问DOM元素是有代价的,修改元素的代价则更为昂贵,因为它会导致浏览器重新计算页面的集合变化
当然,最坏的情况就是在循环中访问或修改元素,尤其是对 HTML 元素集合循环操作
例一
1
2
3
4
5function innerHTMLLoop() {
for(var count = 0; count < 15000; count ++) {
document.getElementById('here').innerHTML += 'a';
}
}例二
1
2
3
4
5
6
7function innerHTMLLoop2() {
var content = '';
for(var count = 0; count < 15000; count ++) {
content += 'a';
}
document.getElementById('here').innerHTML += content;
}结果显而易见,例二中的 innerHTMLLoop2 运行得更快
得出结论,访问DOM的次数越多,代码运行得速度越慢,因此,通用的经验法则是:减少访问 DOM 的次数,把运算尽量留在ECMAScript 这一端处理
innerHTML 对比 DOM 方法
- 在旧版本浏览器中 innerHTML 的优势明显,在新版本的浏览器中两种方式相差无几
节点克隆
- 使用 element.cloneNode() 替代 document。createElemnt(),运行效率会稍快一点,但是差距不大
HTML 集合
- HTML 集合以一种“假定实时态”实时存在,这意味着当底层文档对象更新时,它也会自动更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 一个意外的死循环
var alldivs = document.getElementsByTagName('div');
for(var i = 0; i < alldivs.length; i ++) {
document.body.appendChild(document.createElement('div));
}
/**
* 结局方案
* 将 HTML 集合拷贝到普通数组,再进行操作
*/
function toArray(coll) {
for(var i = 0, a = []; i < coll.length; i ++) {
a[i] = coll[i];
}
return a;
}
var coll = document.getElementsByTagName('div');
var ar = toArray(coll);
昂贵的集合
优化方案:
相同的内容和数量下,便利一个数组的速度明显快于便利一个 HTML 集合
循环时缓存数组长度
遍历 DOM
获取 DOM 元素
在不同浏览器中,elemnt.nextSibling 和 parentEle.childNode 运行时间相差无几
但是在 IE6 中,nextSibling 快16倍
在 IE7 中,nextSibling 快 105 倍
因此得出结论,在老版本的 IE 中,推荐使用 nextSibling 来查找DOM 节点,其他情况取决于个人或团队偏好
元素节点
- 在所有浏览器中,children 都比 childNodes 要快,因为集合项更少
选择器 API
如果需要处理大量组合查询,推荐使用 querySelectorAll()
还有一个新的选择器API,querySelector(),可以获取第一个匹配到的节点
重绘与重排
- 概念:
当DOM的变化影响了元素的几何属性(宽和高)——比如改变边框宽度或给段落增加文字,导致行数增加——浏览器需要重新计算元素的几何数形,同样其他元素的几何属性也会因此受到影响。浏览器回事渲染树中受到影响的部分失效,并重新构建渲染树。这个过程称为“重排”。完成重排后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为“重绘”。
- 并不是所有的 DOM 变化都会影响几何属性,例如,改变一个元素的背景色并不会影响它的宽和高。这种情况下,只会发生一次重绘(不需要重排)
重排何时发生
添加或删除可见的 DOM 元素
元素位置改变
元素尺寸改变(包括:外边距、内边距、边框厚度、宽度、高度等属性改变)
内容改变,例如:文本改变或图片被另一个不同尺寸的图片替代
页面渲染器初始化
浏览器窗口尺寸改变
滚动条出现时
渲染树变化的排队与刷新
由于每次重排都会产生计算消耗,大多数浏览器通过队列化修改并批量执行来优化重拍过程,然而,你可能会(经常不知不觉)强制刷新队列要求计划任务立即执行。获取布局信息的操作会导致队列刷新。例如一下方法:
offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop, scrollLeft, scrollWidth, scrollHeight
clientTop, clientLeft, clientWidth, clientHeight
getComputedStyle() (currentStyle in IE)
以上属性和方法需要最新的布局信息,因此浏览器不得不执行渲染队列的“待处理变化”并触发重排以返回正确的值。
- 因此,在修改样式的过程中,最好避免出现上面列出的属性。它们都会刷新渲染队列,即使你是在获取最近未发生改变的或者与最新改变无关的布局信息
最小化重绘与重排
- 如上所述,通过延迟访问布局信息来避免“重排”
改变样式
合并所有的改变然后依次处理
1
2
3
4
5
6
7
8var el = document.getElementById('box');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';
// 上面可能会导致浏览器触发三次重排
// 改写成下面的方式就会更高效
var el = document.getElementById('box');
el.style.cssText = 'border-left: 1px; border-right: 1px; padding: 5px;'另一个一次性修改样式的方法是修改CSS的class名称,而不是修改内联样式。适用于那些不依赖运行逻辑和计算的情况
1
2var el = document.getElementById('box');
el.className = 'active'
批量修改 DOM
可以使用以下步骤进行优化:
使元素脱离文档流
对其应用多重改变
把元素带回文档中
使 DOM 脱离文档的三种基本方法:
- 前情提要
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<!-- 如下元素 -->
<ul id="myList">
<li><a href="http://www.baidu.com">百度</a></li>
<li><a href="http://www.google.com">谷歌</a></li>
<ul>
<script>
var data = [
{
"name": "Jack",
"url": "https://www.redux.org.cn"
},
{
"name": "Ross",
"url": "https://github.com"
}
]
// 通用函数
function appendDataToElement(appendToElement, data) {
var a, li;
for (var i = 0, max = data.length; i < max; 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.getElementById('myList');
appendDataToElement(ul, data);
</script>
隐藏元素,应用修改,重新显示
1
2
3
4
5// 修改之后的更新方式
var ul = document.getElementById('myList');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';使用文档片段(document fragment)在当前 DOM 之外构建一个子树,再把它拷贝回文档
1
2
3
4
5// 修改之后的更新方式
var ul = document.getElementById('myList');
var fragment = document.creatDocumentFragment();
appendDataToElement(fragment, data);
ul.appendChild(fragment);将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素
1
2
3
4
5// 修改之后的更新方式
var ul = document.getElementById('myList');
var clone = ul.cloneNode(true);
appendDataToElement(clone, data);
ul.parentNode.replaceChild(clone, ul);
缓存布局信息
正如开始所说的,尽量减少获取布局信息的获取次数
应用场景:获取偏移量、滚动位置、计算出的样式值
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 移动元素到一定位置后停止动画
// 低效的
myElement.style.left = 1 + myElement.offsetLeft + 'px';
myElement.style.top = 1 + myElement.offsetTop + 'px';
if (myElement.offsetLeft >= 500) {
stopAnimation();
}
// 改写之后:
// 先将 myElement.offsetLeft 缓存给局部变量 current
var current = myElement.offsetLeft;
....
// 循环体中
current ++;
myElement.style.left = current + 'px';
myElement.style.top = current + 'px';
if (current >= 500) {
stopAnimation();
}
让元素脱离动画流
步骤
使用绝对位置定位页面上的动画元素,将其脱离文档流
让元素动起来。当它扩大时,会临时覆盖部分页面。但这只是页面一个小区域的重绘过程,不会产生重排并重绘页面的大部分内容
当动画结束时恢复定位,从而只会下移一次文档的其他元素
hover 伪类
如果你有大量元素使用了 :hover,那么会降低响应速度。此问题在 IE8 中更为明显
因此在元素很多时应避免使用这种效果,比如很大的表格或很长的列表
事件委托
当页面中存在大量元素,且每一个都要一次或多次绑定事件处理器时,这种情况可能会影响性能。每绑定一个事件处理器都是有代价的,它要么是加重了页面负担(更多的标签或JavaScript代码),要么是增加了运行期的执行时间。
因此,一个简单而优雅的处理 DOM 事件的技术是事件委托。它基于这样一个原理:事件逐层冒泡并能被父级元素捕获
实例代码
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/**
* 场景: <div><ul id="menu"><li><a href="http://www.github.com">menu #1</a></li></ul></div>
* 用户点击链接 “menu #1”,点击事件首先由<a>元素收到,然后向 DOM 树上层冒泡,被<li>元素接受,接着是<ul>,然后是<div>。
* 以此类推,一直到达document 的顶层乃至 window。这使得你可以添加一个事件处理器到父级元素,由它接受所有子节点的事件消息
*
*/
document.getElementById('menu').onclick = function(e) {
// 浏览器 target
e = e || window.event;
var target = e.target || e.srcElement;
var pageid, hrefparts;
// 只关心hrefs, 非链接点击则退出
if (target.nodeName !== 'A') {
return;
}
// 从链接中找出页面 ID
hrefparts = target.href.split('/');
pageid = hrefparts[hrefparts.length - 1];
pageid = pageid.replace('.html', '');
// 更新页面
ajaxReauest('xhr.php?page=' + id, updatePageContents);
// 浏览器阻止默认行为并取消冒泡
if (typeof e.preventDefault === 'function') {
e.preventDefault();
e.stopPropagation();
} else {
e.returnValue = false;
e.cancelBubble = true;
}
}跨浏览器兼容性的部分(可封装到可重用的类库)
访问事件对象,并判断事件源
取消文档树中的冒泡(可选)
阻止默认动作(可选,但本例需要,因为需要捕获并阻止打开链接)
小结
访问和操作 DOM 是现代 Web 应用的重要部分。范每次穿越连接 ECMAScript 和 DOM 两个岛屿之间的前两,都会被收取“过桥费”。为了减少 DOM 编程带来的性能损失,请记住以下几点:
最小化 DOM 访问次数,尽可能在 JavaScript 端处理
如果需要多次访问某个 DOM 节点,请使用局部变量存储它的引用
小心处理 HTML 集合,因为它实时连系着底层文档。把集合的长度缓存到一个变量中,并在迭代中使用。如果需要经常操作集合,建议把它拷贝到一个数组中
如果可能的话,使用速度更快的 API,比如 querySelectorAll()和 firstElementChild
要留意重绘和重排;批量修改样式时,“离线”操作 DOM 树,使用缓存,并减少访问布局信息的次数
动画中使用绝对定位,使用拖放代理
使用事件委托来减少事件处理器的数量