1. 程式人生 > >前端效能優化(JavaScript篇)

前端效能優化(JavaScript篇)

前端效能優化(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()函式內部的字串進行解釋執行,這需要消耗大量的時間。像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篇)中已有描述,就不贅述了

轉載地址:http://blog.segmentfault.com/skyinlayer/1190000000490324