前端性能优化之DOM篇章

前言

前端经常需要和DOM打交道,增加、删除、替换是常见的DOM操作,然而就是这些操作时常成为网站性能的瓶颈,为了提高我们的网站性能,我们需要在DOM上面花不少心思。

1. 使用DOM的引用

在进行DOM操作前,我们先要对元素进行查找,或者需要根据后台返回的数据动态的创建元素,比如我们得到一个数组类型的数据data,需求是根据data往一个id为main的元素中添加li。我们可能会进行下面的DOM操作

1
2
3
4
5
for(var i = 0; i < data.length; i++ ){
var li = document.createElement('li');
li.innerText = data[i];
document.getElementById('main').appendChild(li);
}

上面的代码每次循环都会去计算一个data的length值,查询并得到main元素,可想而之其效率应该有多低下,好的方式是在进行循环之前便将data的length以及main元素缓存下来,在循环的时候使用其引用即可。

1
2
3
4
5
6
7
8
9
var oMain = document.getElementById('main');
var len = data.length;
var i,li;
for(i = 0; i < len; i++ ){
li = document.createElement('li');
li.innerText = data[i];
oMain.appendChild(li);
}

注意元素的查找默认根元素是document,当我们需要对某些元素进行频繁的查找的时候,可以先将某个元素缓存下来,后面的元素查找则是基于该元素,从而减短查找路径

2. 使用文档碎片

上面的操作中还有一个非常耗费性能的地方,oMain.appendChild(li),每次循环都会进行一次元素添加操作,进而导致浏览器重排,我们知道浏览器的重排和重绘是需要耗费大量的时间进行的,所以提高网页性能的一方面是考虑尽量减少重排和重绘的次数。将频繁的DOM操作先在内存中完成,最后一次性将节点推进页面当中,这里我们会用到一个方法document.createDocumentFragment,重新修改上诉例子如下

1
2
3
4
5
6
7
8
9
10
11
12
var oMain = document.getElementById('main');
var frag = document.createDocumentFragment
var len = data.length;
var i,li;
for(i = 0; i < len; i++ ){
li = document.createElement('li');
li.innerText = data[i];
frag.appendChild(li); // 所有的操作在内存中完成,这个时候不会触发重排
}
oMain.appendChild(frag); // 最后一次性添加到页面中,只出发浏览器一次重排

3. 使用innerHTML一次性添加DOM节点

以上的代码中每次都要通过document.createElement('li')创建元素,通过li.innerText = data[i]设置文本信息,蛋疼的很,我们可以尝试着使用innerHTML来一次性添加元素,当然前提是你要先以字符串的形式把元素和数据拼接好

1
2
3
4
5
6
7
8
9
10
var oMain = document.getElementById('main');
var sHtml = '';
var len = data.length;
var i,li;
for(i = 0; i < len; i++ ){
sHtml += '<li>'+ data[i] +'</li>';
}
oMain.innerHTMl = sHtml; // 最后一次性添加到页面中,只出发浏览器一次重排

虽然这样做效率有所提升,但是当页面的DOM结构一旦复杂起来,拼接字符串便会变成一件相当恶心的事情

4. 使用事件代理批量处理事件

事件代理本质上就是将原本自己该干的事情,委托给别人( 这里是指父节点或者祖先节点 )做。
还是用上面的例子,假设我们现在要给每个li都添加上一个点击事件,可能会写出下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var oMain = document.getElementById('main');
var frag = document.createDocumentFragment
var len = data.length;
var i,li;
for(i = 0; i < len; i++ ){
li = document.createElement('li');
li.innerText = data[i];
li.addEventListener('click', function(){
// do something
},false)
frag.appendChild(li);
}
oMain.appendChild(frag);

很显然data的lenth有多大,循环就得执行多少次addEventListener这个函数多少次,当数据量很大的时候效率自然低了不少,所以我们可以尝试使用事件代理的形式将事件委托到main元素上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var oMain = document.getElementById('main');
var sHtml = '';
var len = data.length;
var i,li,target;
for(i = 0; i < len; i++ ){
sHtml += '<li>'+ data[i] +'</li>';
}
oMain.innerHTMl = sHtml; // 最后一次性添加到页面中,只出发浏览器一次重排
oMain.addEventListener('click', function(ev){
target = ev.target || ev.srcElement;
if(target.tagName.toLowerCase() == 'li'){
// do something
}
}, false)

5. 通过className来批量修改元素样式

经常有这样的场景,我们需要在js中批量的修改元素的样式,比如

1
2
3
4
ele.style.width = 100 + 'px';
ele.style.height = 100 + 'px';
ele.style.backgrounfColor = 'red';
ele.style.border = 'solid 1px green';

以上代码会多次出发浏览器重绘和重排,一种好的方式是将需要修改的样式在样式文件中先写好,通过给元素赋值className的形式批量修改样式

1
2
3
4
5
6
.active{
width: 100px;
height: 100px;
backgroung-color: red;
border: solid 1px green;
}

给元素赋值className

1
ele.className += ' active'; // 注意前面的空格