前端性能优化(JavaScript篇)

优化循环

如果现在有个一个data[]数组,需要对其进行遍历,应当怎么做?最简单的代码是:

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

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

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

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

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()函数内部的字符串进行解释运行,这需要消耗大量的时间。像Function、setInterval、setTimeout也是类似的

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缓存

出处:天镶

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • 原文: https://github.com/ecomfe/spec/blob/master/javascript...
    zock阅读 3,371评论 2 36
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,598评论 18 399
  • 大学生开始了大学生活之后,要往前走的话,不需要太关注过去,不需要关注现在所在的大学是好还是坏,不需要关注家庭出身是...
    老何职堂阅读 284评论 0 1
  • 有过做父母经验的人,应该都会有种共识,有时孩子喜欢玩儿的游戏好无聊,做父母的有时真有点“强颜欢笑”的感觉。 那么这...
    有鱼上上签阅读 287评论 0 1
  • 今天儿子放学我有事,孩子他爸去接的。回家听对象说,孩子一放学出来,见到爸爸就说,爸爸我今天考试没考好,只考了79分...
    文皓文文妈妈阅读 222评论 0 0