編寫高效的JavaScript程序

轉載自http://kb.cnblogs.com/page/168162/

英文原文:Writing Fast, Memory-Efficient JavaScript

  Addy Osmani是谷歌公司Chrome團隊中的一名程序開發工程師。他是一位JavaScript愛好者,曾經編寫過一本開放源碼方面的書籍《Learning JavaScript Design Patterns》以及《Developing Backbone Applications》。爲Modernizr和jQuery社區貢獻了開源項目,目前正在從事‘Yeoman’項目,旨在爲開發者提供一系列健壯的工具、程序庫和工作流,幫助他們快速構建出漂亮、引人注目的Web應用。本文作者將帶領大家探索高效編寫代碼的測試驗證方法。

  文章內容如下:

  JavaScript引擎包括Google V8(Chrome,Node)都是專爲快速執行大型JavaScript程序而設計的。在開發過程中,如果你在乎內存使用率和性能情況,那麼你應該會關心在用戶的瀏覽器中JavaScript引擎背後是怎麼樣的。無論是V8、SpiderMonkey (Firefox)、Carakan (Opera)、Chakra (IE) 還是其他,有了它們可以幫助你更好的優化應用程序。

  我們應該時不時地詢問自己:

  • 我還能做些什麼使代碼更加有效?

  • 主流的JavaScript引擎做了哪些優化?

  • 什麼是引擎無法優化的,我能期待利用垃圾回收進行清潔嗎?

 

  快速的加載Web網頁就如同汽車一樣,需要使用特殊工具。

  當涉及到編寫高效的內存和快速創建代碼時總會出現一些常見的弊端,在這篇文章中我們將探索高效編寫代碼的測試驗證方法。

  一、JavaScript如何在V8中工作?

  如果你對JS引擎沒有較深的瞭解,開發一個大型Web應用也沒啥問題,就好比會開車的人也只是看過引擎蓋而沒有看過車蓋內的引擎一樣(這裏將Web網頁比如成汽車)。Chrome瀏覽器是我的優先選擇,這裏我將談下V8的核心組件:

  • 一個基本的編譯器,在代碼執行前分析JavaScript、生成本地機器代碼而非執行字節代碼或是簡單的解釋,該段代碼之初不是高度優化的。

  • V8用對象模型“表述”對象。在JavaScript中,對象是一個關聯數組,但是V8中,對象被“表述”爲隱藏類,這種隱藏類是V8的內部類型,用於優化後的查找。

  • 運行時分析器監視正在運行的系統並優化“hot”(活躍)函數。(比如,終結運行已久的代碼)

  • 通過運行時分析器把優化編譯器重新編譯和被運行時分析器標識爲“hot”的代碼 ,這是一種有效的編譯優化技術,(例如用被調用者的主體替換函數調用的位置)。

  • V8支持去優化,也就是說當你發現一些假設的優化代碼太過樂觀,優化編譯器可以退出已生成的代碼。

  • 垃圾回收,瞭解它是如何工作的,如同優化JavaScript一樣同等重要。

  二、垃圾回收

  垃圾回收是內存管理的一種形式,它試圖通過將不再使用的對象修復從而釋放內存佔用率。垃圾回收語言(比如JavaScript)是指在JavaScript這種垃圾回收語言中,應用程序中仍在被引用的對象不會被清除。手動消除對象引用在大多數情況下是沒有必要的。通過簡單地把變量放在需要它們的地方(理想情況下,儘可能是局部作用域,即它們被使用的函數裏而不是函數外層),一切將運作地很好。

  垃圾回收清除內存

  在JavaScript中強制執行垃圾回收是不可取的,當然,你也不會想這麼做,因爲垃圾回收進程被運行時控制着,它知道什麼時候纔是適合清理代碼的最好時機。

  1. “消除引用”的誤解(De-Referencing Misconceptions)

  在JavaScript中回收內存在網上引發了許多爭論,雖然它可以被用來刪除對象(map)中的屬性(key),但有部分開發者認爲它可以用來強制“消除引用”。建議儘可能避免使用delete,在下面的例子中delete o.x 的弊大於利,因爲它改變了o的隱藏類,使它成爲通用的慢對象。

    var o = { x: 1 };       
    delete o.x; // true     
    o.x; // undefined

  目的是爲了在運行時避免修改活躍對象的結構,JavaScript引擎可以刪除類似“hot”對象,並試圖對其進行優化。如果該對象的結果沒有太大改變,超過生命週期,刪除可能會導致其改變。

  對於null是如何工作也是有誤解的。將一個對象引用設置爲null,並沒有使對象變“空”,只是將它的引用設置爲空而已。使用o.x=null比使用delete會更好些,但可能也不是很必要。

    var o = { x: 1 };      
    o = null;       
    o; // null     
    o.x // TypeError

  如果這個引用是最後一個引用對象,那麼該對象可進行垃圾回收;倘若不是,那麼此方法不可行。注意,無論您的網頁打開多久,全局變量不能被垃圾回收清理。

var myGlobalNamespace = {};

  當你刷新新頁面時,或導航到不同的頁面,關閉標籤頁或是退出瀏覽器,纔可進行全局清理;當作用域不存在這個函數作用域變量時,這個變量纔會被清理,即該函數被退出或是沒有被調用時,變量才能被清理。

  經驗法則:

  爲了給垃圾回收創造機會,儘可能早的收集對象,儘量不要隱藏不使用的對象。這一點主要是自動發生,這裏有幾點需要謹記:

  1. 正如之前我們提到的,手動引用在合適的範圍內使用變量是個更好的選擇,而不是將全局變量清空,只需使用不再需要的局部函數變量。也就是說我們不要爲清潔代碼而擔心。

  2. 確保移除不再需要的事件偵聽器,尤其是當DOM對象將要被移除時。

  3. 如果你正在使用本地數據緩存,請務必清潔該緩存或使用老化機制來避免存儲那些不再使用的大量數據。

  2. 函數(Functions)

  正如我們前面提到的垃圾回收的工作原理是對內存堆中已經死亡的或者長時間沒有使用的對象進行清除和回收。下面的例子能夠更好的說明這一點:

    function foo() {     
        var bar = new LargeObject();      
        bar.someCall();       
    }

  當foo返回時,bar自動指向垃圾回收對象,這是因爲沒被調用,這裏我們將做個對比:

    function foo() {       
        var bar = new LargeObject();      
        bar.someCall();     
      return bar;      
    }  
     
    // somewhere else      
    var b = foo();

  這裏有個調用對象且被一直調用着直到這個調用交給b(或是超出b範圍)。

  3. 閉包(Closures)

  當你看到一個函數返回到內部函數,該內部函數可以訪問外部函數,即使外部函數正在被執行。這基本上是一個封閉的,可以在特定的範圍內設置變量的表達式。比如:

    function sum (x) {  
        function sumIt(y) {  
            return x + y;  
        };  
        return sumIt;  
    }  
       
    // Usage  
    var sumA = sum(4);  
    var sumB = sumA(3);  
    console.log(sumB); // Returns 7

  在sum調用上下文中生成的函數對象(sumIt)是無法被回收的,它被全局變量(sumA)所引用,並且可以通過sumA(n)調用。

  這裏有個示例演示如何訪問largeStr?

    var a = function () {  
        var largeStr = new Array(1000000).join('x');  
        return function () {  
            return largeStr;  
        };  
    }();

  我們可以通過a():

    var a = function () {  
        var smallStr = 'x';  
        var largeStr = new Array(1000000).join('x');  
        return function (n) {  
            return smallStr;  
        };  
    }();

  此時,我們不能訪問了,因爲它是垃圾回收的候選者。

  4. 計時器(Timers)

  最糟糕的莫過於在循環中泄露,或者在setTimeout()/setInterval()中,但這卻是常見的問題之一。

    var myObj = {  
        callMeMaybe: function () {  
            var myRef = this;  
            var val = setTimeout(function () {  
                console.log('Time is running out!');  
                myRef.callMeMaybe();  
            }, 1000);  
        }  
    };

  如果我們運行:

myObj.callMeMaybe();

  在計時器開始前,我們看到每一秒“時間已經不多了”,這時,我們將運行:

myObj = null;

  三、當心性能陷阱

  除非你真正需要,否則永遠不要優化代碼。在V8中你能輕易的看到一些細微的基準測試顯示比如N比M更佳,但是在真實的模塊代碼中或是在實際的應用程序中測試,這些優化所帶來的影響要比你想象中要小的多。

  創建一個模塊,這裏有三點:

  1. 採用本地的數據源包含ID數值

  2. 繪製一個包含這些數據的表格

  3. 添加事件處理程序,當用戶點擊的任何單元格時切換單元格的css class

  如何存儲數據?如何高效的繪製表格並追加到DOM?怎樣處理表單上的事件?

  注意:下面的這段代碼,千萬不能做:

    var moduleA = function () {  
        return {  
       
            data: dataArrayObject,  
       
            init: function () {  
                this.addTable();  
                this.addEvents();  
            },  
       
            addTable: function () {  
       
                for (var i = 0; i < rows; i++) {  
                    $tr = $('<tr></tr>');  
                    for (var j = 0; j < this.data.length; j++) {  
                        $tr.append('<td>' + this.data[j]['id'] + '</td>');  
                    }  
                    $tr.appendTo($tbody);  
                }  
       
            },  
            addEvents: function () {  
                $('table td').on('click', function () {  
                    $(this).toggleClass('active');  
                });  
            }  
       
        };  
    }();

  很簡單,但是卻能把工作完成的很好。

  請注意,直接使用DocumentFragment和本地DOM方法生成表格比使用jQuery更佳,事件委託通常比單獨綁定每個td更具備高性能。jQuery一般在內部使用DocumentFragment,但是在這個例子中,通過內循環調用代碼append() ,因此,無法在這個例子中進行優化,但願這不是一個詬病,但請務必將代碼進行基準測試。

  這裏,我們通過opting for documentFragment提高性能,事件代理對簡單的綁定是一種改進,可選的DocumentFragment也起到了助推作用。

    var moduleD = function () {        
        return {       
            data: dataArray,         
            init: function () {  
                this.addTable();  
                this.addEvents();  
            },  
            addTable: function () {  
                var td, tr;  
                var frag = document.createDocumentFragment();  
                var frag2 = document.createDocumentFragment();  
       
                for (var i = 0; i < rows; i++) {  
                    tr = document.createElement('tr');  
                    for (var j = 0; j < this.data.length; j++) {  
                        td = document.createElement('td');  
                        td.appendChild(document.createTextNode(this.data[j]));  
       
                        frag2.appendChild(td);  
                    }  
                    tr.appendChild(frag2);  
                    frag.appendChild(tr);  
                }  
                tbody.appendChild(frag);  
            },  
            addEvents: function () {  
                $('table').on('click', 'td', function () {  
                    $(this).toggleClass('active');  
                });  
            }         
        };         
    }();

  我們不妨看看其他提供性能的方法,也許你曾讀過使用原型模式或是使用JavaScript模板框架進行高度優化。但是使用這些僅針對可讀的代碼。此外,還有預編譯。我們一起來實踐下:

    moduleG = function () {};         
    moduleG.prototype.data = dataArray;  
    moduleG.prototype.init = function () {  
        this.addTable();  
        this.addEvents();  
    };  
    moduleG.prototype.addTable = function () {  
        var template = _.template($('#template').text());  
        var html = template({'data' : this.data});  
        $tbody.append(html);  
    };  
    moduleG.prototype.addEvents = function () {  
       $('table').on('click', 'td', function () {  
           $(this).toggleClass('active');  
       });  
    };  
       
    var modG = new moduleG();

  事實證明,選擇模板和原型並沒有給我們帶來多大好處。

  四、V8引擎優化技巧:

  特定的模式會導致V8優化產生故障。很多函數無法得到優化,你可以在V8平臺使用--trace-opt file.js搭配d8實用程序。

  如果你關心速度,那麼盡最大努力確保單態函數(functions monomorphic),確保變量(包括屬性,數組和函數參數)只適應同樣的隱藏類包含的對象。

  下面的代碼演示了,我們不可這麼做:

    function add(x, y) {  
       return x+y;  
    }  
       
    add(1, 2);  
    add('a','b');  
    add(my_custom_object, undefined);

  未初始化時不要加載和執行刪除操作,因爲它們並沒有輸出差異,這樣做反而會使程序變得更慢。不要編寫大量函數,函數越多越難優化。

  1. Objects使用技巧:

  適應構造函數來創建對象。這將確保所創建的所有對象具備相同的隱藏類並有幫助避免更改這些類。

  在程序或者複雜性上不要限制多種對象類型。(原因:長原型鏈中傾向於傷害,只有極少數的對象屬性得到一個特殊的委託)對於活躍對象保持短原型鏈以及低字段計數。

  2. 對象克隆(Object Cloning)

  對象克隆對於應用開發者來說是一種常見的現象。雖然在V8中這是實現各種類型問題的基準,但是當你進行復制時,一定要當心。當複製較大的程序時通常很會慢,因此,儘量不要這麼做。在JavaScript循環中此舉是非常糟糕的。這裏有個最快的技巧方案,你不妨學習下:

    function clone(original) {  
      this.foo = original.foo;  
      this.bar = original.bar;  
    }  
    var copy = new clone(original);

  3. 模塊模式中的緩存功能

  在模塊模式中使用緩存功能也許在性能方面會有所提升。請參閱下面的例子,通過jsPerf test測試。注,使用這種方法比依靠原型模式更佳。

Performance improvements

  推薦:這是測試原型與模塊模式性能代碼

      // Prototypal pattern  
      Klass1 = function () {}  
      Klass1.prototype.foo = function () {  
          log('foo');  
      }  
      Klass1.prototype.bar = function () {  
          log('bar');  
      }  
       
      // Module pattern  
      Klass2 = function () {  
          var foo = function () {  
              log('foo');  
          },  
          bar = function () {  
              log('bar');  
          };  
       
          return {  
              foo: foo,  
              bar: bar  
          }  
      }  
       
       // Module pattern with cached functions  
      var FooFunction = function () {  
          log('foo');  
      };  
      var BarFunction = function () {  
          log('bar');  
      };  
       Klass3 = function () {  
          return {  
              foo: FooFunction,  
              bar: BarFunction  
          }  
      }  
       
       // Iteration tests  
       
      // Prototypal  
      var i = 1000,  
          objs = [];  
      while (i--) {  
          var o = new Klass1()  
          objs.push(new Klass1());  
          o.bar;  
          o.foo;  
      }  
       
      // Module pattern  
      var i = 1000,  
          objs = [];  
      while (i--) {  
          var o = Klass2()  
          objs.push(Klass2());  
          o.bar;  
          o.foo;  
      }  
       // Module pattern with cached functions  
      var i = 1000,  
          objs = [];  
      while (i--) {  
          var o = Klass3()  
          objs.push(Klass3());  
          o.bar;  
          o.foo;  
      }  
    // See the test for full details

  4. 數組使用技巧:

  一般情況下,我們不要刪除數組元素。它是使數組過渡到較慢的內部表現形式。當密鑰集變得稀疏時,V8最終將切換到字典模式,這是變慢的原因之一。

An old phone on the screen of an iPad.

  五、應用優化技巧:

  在Web應用領域裏,速度就是一切。沒有用戶希望在啓動電子表格時需要等上幾秒鐘,或者花上幾分鐘時間來整理信息。這也是爲什麼在性能方面,需要格外注意的一點,有人甚至將編碼階段稱爲至關重要的一部分。

  理解和提升性能方面是非常有用的,但它也有一定的難度。這裏推薦幾個步驟來幫你解決:

  1. 測試:在應用程序中找到慢的節點 (~45%)

  2. 理解:查找問題所在(~45%)

  3. 修復:(~10%)

  當然,還有許多工具或是技術方案幫助解決以上這些問題:

  1. 基準測試。在JavaScript上有許多方法可進行基準測試。

  2. 剖析。Chrome開發工具能夠很好的支持JavaScript分析器。你可以使用這些性能進行檢測,哪些功能佔用的時間比較長,然後對其進行優化。最重要的是,即使是很小的改變也能影響整體的表現。關於這款分析工具這裏有份詳細的介紹。

  3. 避免內存泄露-3快照技術。谷歌開發團隊通常會使用Chrome開發工具包括Gmail來幫助他們發現和修復內存泄露;此外,3 snapshot也是不錯的選擇。該weixinqunkong8.com技術允許在程序中記錄一些行爲、強制垃圾回收、查詢,如果DOM節點無法返回預期的基線上,3 snapshot幫助分析確定是否存在內存泄露。

  4. 單頁面程序上的內存管理。當你在編寫單頁程序時(比如,AngularJS,Backbone,Ember)),內存管理非常重要,他們從未得到刷新。這就意味着內存泄露很明顯。在移動單頁程序上存在着巨大的陷阱,因爲內存有限,長期運行的程序比如email客戶端或者社交網絡應用。因此,它肩負着巨大的責任。

  Derick發表了這篇《memory pitfalls》教您如何使用Backbone.js以及如何進行修復。Felix Geisendrfer的這篇在Node中調試內存泄露也值得一讀。

  5. 最小化迴流迴流是指在瀏覽器中用戶阻止此操作,所以它是有助於理解如何提高迴流時間,你可以使用DocumentFragment一個輕量級的文檔對象來處理。

  6. Javascript內存泄露檢測器。由Marja Hltt和Jochen Eisinger兩人開發的這款工具,你不妨試試。

  7. V8 flags調試優化和內存回收。Chrome支持通過flags和js-flags flag獲取更詳細的輸出:

  例如:

"/Applications/Google Chrome/Google Chrome" --js-flags="--trace-opt --trace-deopt"

  Windows用戶運行chrome.exe --js-flags="--trace-opt --trace-deopt"。

  當開發應用時,可以使用下面的V8 flags:

  • trace-opt –日誌名稱的優化功能,顯示優化跳過的代碼

  • trace-deopt –記錄運行時將要“去優化”的代碼。

  • trace-gc – 對每次垃圾回收時進行跟蹤

Measuring.

  結束語:

  正如上面提到的,在JavaScript引擎中有許多隱藏的陷阱,世界上沒有什麼好的銀彈能夠幫助你提高性能,只有通過在測試環境中進行優化,實現最大的性能收益。因此,瞭解引擎如何輸出和優化代碼可以幫助你調整應用程序。

  因此,測試它、理解它、修復它,如此往復!




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章