前端性能优化(JavaScript篇)

正巧看到在送书,因而乎找了找本身博客上记录过的一些东西来及其无耻的蹭书了~~~javascript

小广告:更多内容能够看个人博客html

优化循环

若是如今有个一个data[]数组,须要对其进行遍历,应当怎么作?最简单的代码是:前端

for (var i = 0; i < data.length; i++) {
    //do someting
}

这里每次循环开始前都须要判断i是否小于data.length,JavaScript并不会对data.length进行缓存,而是每次比较都会进行一次取值。如咱们所知,JavaScript数组实际上是一个对象,里面有个length属性,因此这里实际上就是取得对象的属性。若是直接使用变量的话就会少一次索引对象,若是数组的元素不少,效率提高仍是很可观的。因此咱们一般将代码改为以下所示:java

for(var i = 0, m = data.length; i < m; i++) {
    //do someting
}

这里多加了一个变量m用于存放data.length属性,这样就能够在每次循环时,减小一次索引对象,可是代价是增长了一个变量的空间,若是遍历不要求顺序,咱们甚至能够不用m这个变量存储长度,在不要求顺序的时候可使用以下代码:web

for(var i = data.length; i--; ) {
    //do someting
}

固然咱们可使用while来替代:正则表达式

var i = data.length;
while(i--) {
    //do someting
}

这样就可只使用一个变量了算法

运算结果缓存

因为JavaScript中的函数也是对象(JavaScript中一切都是对象),因此咱们能够给函数添加任意的属性。这也就为咱们提供符合备忘录模式的缓存运算结果的功能,好比咱们有一个须要大量运算才能得出结果的函数以下:数组

function calculator(params) {
    //大量的耗时的计算 
    return result;
}

若是其中不涉及随机,参数同样时所返回的结果一致,咱们就能够将运算结果进行缓存从而避免重复的计算:浏览器

function calculator(params) {
    var cacheKey = JSON.stringify(params);
    var cache = calculator.cache = calculator.cache || {};
    if(typeof cache[cacheKey] !== 'undefined') {
        return cache[cacheKey];
    }
    //大量耗时的计算
    cache[cacheKey] = result;
    return result;
}

这里将参数转化为JSON字符串做为key,若是这个参数已经被计算过,那么就直接返回,不然进行计算。计算完毕后再添加入cache中,若是须要,能够直接查看cache的内容:calculator.cache缓存

这是一种典型的空间换时间的方式,因为浏览器的页面存活时间通常不会很长,占用的内存会很快被释放(固然也有例外,好比一些WEB应用),因此能够经过这种空间换时间的方式来减小响应时间,提高用户体验。这种方式并不适用于以下场合:
1. 相同参数可能产生不一样结果的状况(包含随机数之类的)
2. 运算结果占用特别多内存的状况

不要在循环中建立函数

这个很好理解,每建立一个函数对象是须要大批量空间的。因此在一个循环中建立函数是很不明智的,尽可能将函数移动到循环以前建立,好比以下代码:

for(var i = 0, m = data.length; i < m; i++) {
    handlerData(data[i], function(data){
        //do something
    });
}

就能够修改成:

var handler = function(data){
    //do something
};
for(var i = 0, m = data.length; i < m; i++) {
    handlerData(data[i], handler);
}

让垃圾回收器回收那些再也不须要的对象

以前我曾在 浅谈V8引擎中的垃圾回收机制 中讲到了V8引擎如何进行垃圾回收。能够从中看到,若是长时间保存对象,老生代中占用的空间将增大,每次在老生代中的垃圾回收过程将会至关漫长。而垃圾回收器判断一个对象为活对象仍是死对象,是按照是否有活对象或根对象含有对它的引用来断定的。若是有根对象或者活对象引用了这个对象,它将被断定为活对象。因此咱们须要经过手动消除这些引用来让垃圾回收器对回收这些对象。

delete

一种方式是经过delete方式来消除对象中的键值对,从而消除引用。但这种方式并不提倡,它会改变对象的结构,可能致使引擎中对对象的存储方式变动,降级为字典方式进行存储(详细请见V8 之旅:对象表示),不利于JavaScript引擎的优化,因此尽可能减小使用

null

另外一种方式是经过将值设为null来消除引用。经过将变量或对象的属性设为null,能够消除引用,使本来引用的对象成为一个“孤岛”,而后在垃圾回收的时候对其进行回收。这种方式不会改变对象的结构,比使用delete要好

全局对象

另外须要注意的是,垃圾回收器认为根对象永远是活对象,永远不会对其进行垃圾回收。而全局对象就是根对象,因此全局做用域中的变量将会一直存在

事件处理器的回收

在日常写代码的时候,咱们常常会给一个DOM节点绑定事件处理器,但有时候咱们不须要这些事件处理器后,就无论它们了,它们默默的在内存中保存着。因此在某些DOM节点绑定的事件处理器不须要后,咱们应当销毁它们。同时绑定的时候也尽可能使用事件代理的方式进行绑定,以避免形成屡次重复的绑定致使内存空间的浪费,事件代理可见前端性能优化(DOM操做篇)

闭包致使的内存泄露

JavaScript的闭包能够说便是“天使”又是“魔鬼”,它“天使”的一面是咱们能够经过它突破做用域的限制,而其魔鬼的一面就是和容易致使内存泄露,好比以下状况:

var result = (function() {
    var small = {};
    var big = new Array(10000000);
    //do something
    return function(){
        if(big.indexOf("someValue") !== -1) {
            return null;
        } else {
            return small;
        }
    }
})();

这里,建立了一个闭包。使得返回的函数存储在result中,而result函数可以访问其做用域内的small对象和big对象。因为big对象和small对象均可能被访问,因此垃圾回收器不会去碰这两个对象,它们不会被回收。咱们将上述代码改为以下形式:

var result = (function() {
    var small = {};
    var big = new Array(10000000);
    var hasSomeValue;
    //do something
    hasSomeValue = big.indexOf("someValue") !== -1;
    return function(){
        if(hasSomeValue) {
            return null;
        } else {
            return small;
        }
    }
})();

这样,函数内部只可以访问到hasSomeValue变量和small变量了,big没有办法经过任何形式被访问到,垃圾回收器将会对其进行回收,节省了大量的内存。

慎用eval和with

Douglas Crockford将eval比做魔鬼,确实在不少方面咱们能够找到更好地替代方式。使用它时须要在运行时调用解释引擎对eval()函数内部的字符串进行解释运行,这须要消耗大量的时间。像FunctionsetIntervalsetTimeout也是相似的

Douglas Crockford也不建议使用with,with会下降性能,经过with包裹的代码块,做用域链将会额外增长一层,下降索引效率

对象的优化

缓存须要被使用的对象

JavaScript获取数据的性能有以下顺序(从快到慢):变量获取 > 数组下标获取(对象的整数索引获取) > 对象属性获取(对象非整数索引获取)。咱们能够经过最快的方式代替最慢的方式:

var body = document.body;
var maxLength = someArray.length;
//...

须要考虑,做用域链和原型链中的对象索引。若是做用域链和原型链较长,也须要对所须要的变量继续缓存,不然沿着做用域链和原型链向上查找时也会额外消耗时间

缓存正则表达式对象

须要注意,正则表达式对象的建立很是消耗时间,尽可能不要在循环中建立正则表达式,尽量多的对正则表达式对象进行复用

考虑对象和数组

在JavaScript中咱们可使用两种存放数据:对象和数组。因为JavaScript数组能够存听任意类型数据这样的灵活性,致使咱们常常须要考虑什么时候使用数组,什么时候使用对象。咱们应当在以下状况下作出考虑:
1. 存储一串相同类型的对象,应当使用数组
2. 存储一堆键值对,值的类型多样,应当使用对象
3. 全部值都是经过整数索引,应当使用数组

数组使用时的优化

  1. 往数组中插入混合类型很容易下降数组使用的效率,尽可能保持数组中元素的类型一致
  2. 若是使用稀疏数组,它的元素访问将远慢于满数组的元素访问。由于V8为了节省空间,会将稀疏数组经过字典方式保存在内存中,节约了空间,但增长了访问时间

对象的拷贝

须要注意的是,JavaScript遍历对象和数组时,使用for...in的效率至关低,因此在拷贝对象时,若是已知须要被拷贝的对象的属性,经过直接赋值的方式比使用for...in方式要来得快,咱们能够经过定一个拷贝构造函数来实现,好比以下代码:

function copy(source){
    var result = {};
    var item;
    for(item in source) {
        result[item] = source[item];
    }
    return result;
}
var backup = copy(source);

可修改成:

function copy(source){
    this.property1 = source.property1;
    this.property2 = source.property2;
    this.property3 = source.property3;
    //...
}
var backup = new copy(source);

字面量代替构造函数

JavaScript能够经过字面量来构造对象,好比经过[]构造一个数组,{}构造一个对象,/regexp/构造一个正则表达式,咱们应当尽力使用字面量来构造对象,由于字面量是引擎直接解释执行的,而若是使用构造函数的话,须要调用一个内部构造器,因此字面量略微要快一点点。

缓存AJAX

曾经听过一个访问时间比较(固然不精确):
* cpu cache ≈ 100 * 寄存器
* 内存 ≈ 100 * cpu cache
* 外存 ≈ 100 * 内存
* 网络 ≈ 100 * 外存

可看到访问网络资源是至关慢的,而AJAX就是JavaScript访问网络资源的方式,因此对一些AJAX结果进行缓存,能够大大减小响应时间。那么如何缓存AJAX结果呢

函数缓存

咱们可使用前面缓存复杂计算函数结果的方式进行缓存,经过在函数对象上构造cache对象,原理同样,这里略过。这种方式是精确到函数,而不精确到请求

本地缓存

HTML5提供了本地缓存sessionStorage和localStorage,区别就是前者在浏览器关闭后会自动释放,然后者则是永久的,不会被释放。它提供的缓存大小以MB为单位,比cookie(4KB)要大得多,因此咱们能够根据AJAX数据的存活时间来判断是存放在sessionStorage仍是localStorage当中,在这里以存储到sessionStorage中为例(localStorage只需把第一行的window.sessionStorage修改成window.localStorage):

function(data, url, type, callback){
    var storage = window.sessionStorage;
    var key = JSON.stringify({
        url : url,
        type : type,
        data : data
    });
    var result = storage.getItem(key);
    var xhr;
    if (result) {
        callback.call(null, result);
    } else {
        xhr.onreadystatechange = function(){
            if(xhr.readyState === 4){
                if(xhr.status === 200){
                    storage.setItem(key, xhr.responseText);
                    callback.call(null, xhr.responseText);
                } else {
                }
            }
        };
        xhr.open(type, url, async);
        xhr.send(data);
    }
};

使用布尔表达式的短路

在不少语言中,若是bool表达式的值已经能经过前面的条件肯定,那么后面的判断条件将再也不会执行,好比以下代码

function calCondition(params) {
    var result;
    //do lots of work
    return !!result;
}

if(otherCondition && calCondition(someParams)) {
    console.log(true);
} else {
    console.log(false);
}

这里首先会计算otherCondition的值,若是它为false,那么整个正则表达式就为false了,后续的须要消耗大量时间的calCondition()函数就不会被调用和计算了,节省了时间

使用原生方法

在JavaScript中,大多数原生方法是使用C++编写的,比js写的方法要快得多,因此尽可能使用诸如Math之类的原生对象和方法

字符串拼接

在IE和FF下,使用直接+=的方式或是+的方式进行字符串拼接,将会很慢。咱们能够经过Array的join()方法进行字符串拼接。不过并非全部浏览器都是这样,如今不少浏览器使用+=比join()方法还要快

使用web worker

web worker是HTML5提出的一项新技术,经过多线程的方式为JavaScript提供并行计算的能力,经过message的方式进行相互之间的信息传递,我尚未仔细研究过

JavaScript文件的优化

使用CDN

在编写JavaScript代码中,咱们常常会使用库(jQuery等等),这些JS库一般不会对其进行更改,咱们能够将这些库文件放在CDN(内容分发网络上),这样能大大减小响应时间

压缩与合并JavaScript文件

在网络中传输JS文件,文件越长,须要的时间越多。因此在上线前,一般都会对JS文件进行压缩,去掉其中的注释、回车、没必要要的空格等多余内容,若是经过uglify的算法,还能够缩减变量名和函数名,从而将JS代码压缩,节约传输时的带宽。另外常常也会将JavaScript代码合并,使全部代码在一个文件之中,这样就可以减小HTTP的请求次数。合并的原理和sprite技术相同

使用Application Cache缓存

这个在以前的文章前端性能优化(Application Cache篇)中已有描述,就不赘述了