編寫高性能JavaScript


  英文鏈接:Writing Fast, Memory-Efficient JavaScript

  非常多JavaScript引擎,如Google的V8引擎(被Chrome和Node所用)。是專門為須要高速運行的大型JavaScript應用所設計的。假設你是一個開發人員。而且關心內存使用情況與頁面性能。你應該了解用戶瀏覽器中的JavaScript引擎是怎樣運作的。

不管是V8,SpiderMonkey的(Firefox)的Carakan(Opera),Chakra(IE)或其它引擎,這樣做能夠幫助你更好地優化你的應用程序

這並非說應該專門為某一瀏覽器或引擎做優化。千萬別這么做。

  可是,你應該問自己幾個問題:

  • 在我的代碼里,能否夠使代碼更高效一些
  • 主流的JavaScript引擎都做了哪些優化
  • 什么是引擎無法優化的,垃圾回收器(GC)能否回收我所期望的東西

載入高速的站點就像是一輛高速的跑車。須要用到特別定制的零件. 圖片來源: dHybridcars.

  編寫高性能代碼時有一些常見的陷阱,在這篇文章中,我們將展示一些經過驗證的、更好的編寫代碼方式。

  那么,JavaScript在V8里是怎樣工作的?

  假設你對JS引擎沒有較深的了解,開發一個大型Web應用也沒啥問題。就好比會開車的人也僅僅是看過引擎蓋而沒有看過車蓋內的引擎一樣。

鑒於Chrome是我的瀏覽器首選,所以談一下它的JavaScript引擎。

V8是由下面幾個核心部分組成:

  • 一個主要的編譯器,它會在代碼執行前解析JavaScript代碼並生成本地機器碼,而不是執行字節碼或簡單地解釋它。這些代碼最開始並非高度優化的。
  • V8將對象構建為對象模型。在JavaScript中對象表現為關聯數組,可是在V8中對象被看作是隱藏的類。一個為了優化查詢的內部類型系統。
  • 執行時分析器監視正在執行的系統,並標識了“hot”的函數(比如花費非常長時間執行的代碼)。
  • 優化編譯器又一次編譯和優化那些被執行時分析器標識為“hot”的代碼,並進行“內聯”等優化(比如用被調用者的主體替換函數調用的位置)。
  • V8支持去優化。這意味着優化編譯器如果發現對於代碼優化的如果過於樂觀。它會舍棄優化過的代碼。
  • V8有個垃圾收集器,了解它是怎樣工作的和優化JavaScript一樣重要。

  垃圾回收

  垃圾回收是內存管理的一種形式。事實上就是一個收集器的概念。嘗試回收不再被使用的對象所占用的內存。在JavaScript這樣的垃圾回收語言中。應用程序中仍在被引用的對象不會被清除。

  手動消除對象引用在大多數情況下是沒有必要的。通過簡單地把變量放在須要它們的地方(理想情況下,盡可能是局部作用域。即它們被使用的函數里而不是函數外層)。一切將運作地非常好。

垃圾回收器嘗試回收內存. 圖片來源: Valtteri Mäki.

  在JavaScript中,是不可能強制進行垃圾回收的。

你不應該這么做,由於垃圾收集過程是由執行時控制的,它知道什么是最好的清理時機。

  “消除引用”的誤解

  網上有很多關於JavaScript內存回收的討論都談到delete這個keyword,盡管它能夠被用來刪除對象(map)中的屬性(key)。但有部分開發人員覺得它能夠用來強制“消除引用”。

建議盡可能避免使用delete。在以下的樣例中delete o.x 的弊大於利,由於它改變了o的隱藏類。並使它成為一個"慢對象"。

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

  你會非常easy地在流行的JS庫中找到引用刪除——這是具有語言目的性的。這里須要注意的是避免在執行時改動”hot”對象的結構。JavaScript引擎能夠檢測出這樣的“hot”的對象。並嘗試對其進行優化。假設對象在生命周期中其結構沒有較大的改變,引擎將會更easy優化對象,而delete操作實際上會觸發這樣的較大的結構改變。因此不利於引擎的優化。

  對於null是怎樣工作也是有誤解的。

將一個對象引用設置為null,並沒有使對象變“空”。僅僅是將它的引用設置為空而已。

使用o.x= null比使用delete會更好些。但可能也不是非常必要。

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

  假設此引用是當前對象的最后引用。那么該對象將被作為垃圾回收。

假設此引用不是當前對象的最后引用,則該對象是可訪問的且不會被垃圾回收。

  另外須要注意的是。全局變量在頁面的生命周期里是不被垃圾回收器清理的。

不管頁面打開多久。JavaScript執行時全局對象作用域中的變量會一直存在。

var myGlobalNamespace = {};

  全局對象僅僅會在刷新頁面、導航到其它頁面、關閉標簽頁或退出瀏覽器時才會被清理。函數作用域的變量將在超出作用域時被清理。即退出函數時,已經沒有不論什么引用。這種變量就被清理了。

  經驗法則

  為了使垃圾回收器盡早收集盡可能多的對象,不要hold着不再使用的對象

這里有幾件事須要記住:

  • 正如前面提到的。在合適的范圍內使用變量是手動消除引用的更好選擇。即一個變量僅僅在一個函數作用域中使用。就不要在全局作用域聲明它。這意味着更干凈省心的代碼。

  • 確保解綁那些不再須要的事件監聽器,尤其是那些即將被銷毀的DOM對象所綁定的事件監聽器。
  • 假設使用的數據緩存在本地,確保清理一下緩存或使用老化機制,以避免大量不被重用的數據被存儲。

  函數

  接下來,我們談談函數。

正如我們已經說過,垃圾收集的工作原理,是通過回收不再是訪問的內存塊(對象)。為了更好地說明這一點,這里有一些樣例。

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();

  如今我們有一個引用指向bar對象。這樣bar對象的生存周期就從foo的調用一直持續到調用者指定別的變量b(或b超出范圍)。

  閉包(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()訪問largeStr,所以它沒有被回收。以下這個呢?

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

  我們不能再訪問largeStr了,它已經是垃圾回收候選人了。【譯者注:由於largeStr已不存在外部引用了】

  定時器

  最糟的內存泄漏地方之中的一個是在循環中。或者在setTimeout()/ setInterval()中。但這是相當常見的。思考以下的樣例:

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

  假設我們執行myObj.callMeMaybe();來啟動定時器,能夠看到控制台每秒打印出“Time is running out!”。假設接着執行myObj = null,定時器依然處於激活狀態。

為了可以持續運行,閉包將myObj傳遞給setTimeout,這樣myObj是無法被回收的。相反。它引用到myObj的由於它捕獲了myRef。這跟我們為了保持引用將閉包傳給其它的函數是一樣的。

  相同值得牢記的是。setTimeout/setInterval調用(如函數)中的引用。將須要運行和完畢,才干夠被垃圾收集。

  當心性能陷阱

  永遠不要優化代碼,直到你真正須要。

如今常常能夠看到一些基准測試。顯示N比M在V8中更為優化,可是在模塊代碼或應用中測試一下會發現,這些優化真正的效果比你期望的要小的多。

做的過多還不如什么都不做. 圖片來源: Tim Sheerman-Chase.

  比方我們想要創建這樣一個模塊:

  • 須要一個本地的數據源包括數字ID
  • 繪制包括這些數據的表格
  • 加入事件處理程序,當用戶點擊的不論什么單元格時切換單元格的css class

  這個問題有幾個不同的因素,盡管也非常easy解決。

我們怎樣存儲數據,怎樣高效地繪制表格而且append到DOM中,怎樣更優地處理表格事件?

  面對這些問題最開始(天真)的做法是使用對象存儲數據並放入數組中,使用jQuery遍歷數據繪制表格並append到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');
            });
        }
    };
}();

  這段代碼簡單有效地完畢了任務。

  但在這樣的情況下,我們遍歷的數據僅僅是本應該簡單地存放在數組中的數字型屬性ID。有趣的是,直接使用DocumentFragment和本地DOM方法比使用jQuery(以這樣的方式)來生成表格是更優的選擇。當然。事件代理比單獨綁定每一個td具有更高的性能。

  要注意盡管jQuery在內部使用DocumentFragment,可是在我們的樣例中,代碼在循環內調用append而且這些調用涉及到一些其它的小知識。因此在這里起到的優化作用不大。

希望這不會是一個痛點。但請務必進行基准測試,以確保自己代碼ok。

  對於我們的樣例,上述的做法帶來了(期望的)性能提升。

事件代理對簡單的綁定是一種改進。可選的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');
            });
        }
    };
}();

  接下來看看其它提升性能的方式。你或許以前在哪讀到過使用原型模式比模塊模式更優。或聽說過使用JS模版框架性能更好。

有時的確如此。只是使用它們事實上是為了代碼更具可讀性。對了。還有預編譯!讓我們看看在實踐中表現的怎樣?

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();

  事實證明。在這樣的情況下的帶來的性能提升能夠忽略不計。模板和原型的選擇並沒有真正提供很多其它的東西。

也就是說,性能並非開發人員使用它們的原因,給代碼帶來的可讀性、繼承模型和可維護性才是真正的原因。

  更復雜的問題包含高效地在canvas上繪制圖片和操作帶或不帶類型數組的像素數據。

  在將一些方法用在你自己的應用之前,一定要多了解這些方案的基准測試。或許有人還記得JS模版的shoot-off隨后的擴展版。你要搞清楚基准測試不是存在於你看不到的那些虛擬應用,而是應該在你的實際代碼中去測試帶來的優化。

  V8優化技巧

  具體介紹了每一個V8引擎的優化點在本文討論范圍之外,當然這里也有很多值得一提的技巧。記住這些技巧你就能降低那些性能低下的代碼了。

  • 特定模式能夠使V8擺脫優化的困境,比方說try-catch。

    欲了解很多其它有關哪些函數能或不能進行優化,你能夠在V8的腳本工具d8中使用–trace-opt file.js命令。

  • 假設你關心速度。盡量使你的函數職責單一,即確保變量(包括屬性,數組,函數參數)僅僅使用同樣隱藏類包括的對象。舉個樣例,別這么干:
function add(x, y) {
   return x+y;
}
add(1, 2);
add('a','b');
add(my_custom_object, undefined);
  • 不要載入未初始化或已刪除的元素。假設這么做也不會出現什么錯誤。可是這樣會使速度變慢。
  • 不要使函數體過大。這樣會使得優化更加困難。

  很多其它內容能夠去看Daniel Clifford在Google I/O的分享 Breaking the JavaScript Speed Limit with V8。 Optimizing For V8 — A Series也很值得一讀。

  對象VS數組:我應該用哪個?

  • 假設你想存儲一串數字,或者一些同樣類型的對象,使用一個數組。
  • 假設你語義上須要的是一堆的對象的屬性(不同類型的),使用一個對象和屬性。這在內存方面很高效,速度也相當快。
  • 整數索引的元素,不管存儲在一個數組或對象中,都要比遍歷對象的屬性快得多
  • 對象的屬性比較復雜:它們能夠被setter們創建。具有不同的枚舉性和可寫性。數組中則不具有如此的定制性,而僅僅存在有和無這兩種狀態。在引擎層面,這同意很多其它存儲結構方面的優化。特別是當數組中存在數字時,比如當你須要容器時。不用定義具有x。y,z屬性的類,而僅僅用數組就能夠了。

  JavaScript中對象和數組之間僅僅有一個的主要差別,那就是數組奇妙的length屬性。假設你自己來維護這個屬性。那么V8中對象和數組的速度是一樣快的。

  使用對象時的技巧

  • 使用一個構造函數來創建對象。這將確保它創建的全部對象具有同樣的隱藏類。並有助於避免更改這些類。

    作為一個額外的優點,它也略快於Object.create()

  • 你的應用中,對於使用不同類型的對象和其復雜度(在合理的范圍內:長原型鏈往往是有害的。呈現僅僅有一個極少數屬性的對象比大對象會快一點)是有沒限制的。對於“hot”對象,盡量保持短原型鏈。而且少屬性。

  對象克隆

  對於應用程序開發者,對象克隆是一個常見的問題。

盡管各種基准測試能夠證明V8對這個問題處理得非常好,但仍要小心。復制大的東西一般是較慢的——不要這么做。

JS中的for..in循環尤其糟糕,由於它有着惡魔般的規范,而且不管是在哪個引擎中,都可能永遠不會比不論什么對象快。

  當你一定要在關鍵性能代碼路徑上復制對象時,使用數組或一個自己定義的“拷貝構造函數”功能明白地復制每一個屬性。這可能是最快的方式:

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

  模塊模式中緩存函數

  使用模塊模式時緩存函數。可能會導致性能方面的提升。

參閱以下的樣例,由於它總是創建成員函數的新副本。你看到的變化可能會比較慢。

  另外請注意,使用這樣的方法明顯更優。不不過依靠原型模式(經過jsPerf測試確認)。

使用模塊模式或原型模式時的性能提升

  這是一個原型模式與模塊模式的性能對照測試

  // 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

  使用數組時的技巧

  接下來說說數組相關的技巧。在普通情況下,不要刪除數組元素,這樣將使數組過渡到較慢的內部表示。當索引變得稀疏,V8將會使元素轉為更慢的字典模式。

  數組字面量

  數組字面量很實用,它能夠暗示VM數組的大小和類型。它通經常使用在體積不大的數組中。

// Here V8 can see that you want a 4-element array containing numbers:
var a = [1, 2, 3, 4];
// Don't do this:
a = []; // Here V8 knows nothing about the array
for(var i = 1; i <= 4; i++) {
     a.push(i);
}

  存儲單一類型VS多類型

  將混合類型(比方數字、字符串、undefined、true/false)的數據存在數組中絕不是一個好想法。比如var arr = [1, “1”, undefined, true, “true”]

  類型判斷的性能測試

  正如我們所示結果。整數的數組是最快的。

  稀疏數組與滿數組

  當你使用稀疏數組時,要注意訪問元素將遠遠慢於滿數組。由於V8不會分配一整塊空間給僅僅用到部分空間的數組。取而代之的是,它被管理在字典中,既節約了空間,但花費訪問的時間。

  稀疏數組與滿數組的測試

  預分配空間VS動態分配

  不要預分配大數組(如大於64K的元素)。其最大的大小,而應該動態分配。

在我們這篇文章的性能測試之前,請記住這僅僅適用部分JavaScript引擎。

空字面量與預分配數組在不同的瀏覽器進行測試

  Nitro (Safari)對預分配的數組更有利。

而在其它引擎(V8,SpiderMonkey)中,預先分配並非高效的。

  預分配數組測試

// Empty array
var arr = [];
for (var i = 0; i < 1000000; i++) {
    arr[i] = i;
}
// Pre-allocated array
var arr = new Array(1000000);
for (var i = 0; i < 1000000; i++) {
    arr[i] = i;
}

  優化你的應用

  在Web應用的世界中。速度就是一切。沒實用戶希望用一個要花幾秒鍾計算某列總數或花幾分鍾匯總信息的表格應用。這是為什么你要在代碼中壓榨每一點性能的重要原因。

圖片來源: Per Olof Forsberg.

  理解和提高應用程序的性能是很實用的同一時候,它也是困難的。我們推薦下面的步驟來解決性能的痛點:

  • 測量:在您的應用程序中找到慢的地方(約45%)
  • 理解:找出實際的問題是什么(約45%)
  • 修復它! (約10%)

  以下推薦的一些工具和技術能夠協助你。

  基准化(BENCHMARKING)

  有非常多方式來執行JavaScript代碼片段的基准測試其性能——一般的如果是。基准簡單地比較兩個時間戳。這中模式被jsPerf團隊指出,並在SunSpiderKraken的基准套件中使用:

var totalTime,
    start = new Date,
    iterations = 1000;
while (iterations--) {
  // Code snippet goes here
}
// totalTime → the number of milliseconds taken
// to execute the code snippet 1000 times
totalTime = new Date - start;

  在這里,要測試的代碼被放置在一個循環中。並執行一個設定的次數(比如6次)。在此之后,開始日期減去結束日期,就得出在循環中執行操作所花費的時間。

  然而。這種基准測試做的事情過於簡單了,特別是假設你想執行在多個瀏覽器和環境的基准。垃圾收集器本身對結果是有一定影響的。即使你使用window.performance這種解決方式。也必須考慮到這些缺陷。

  無論你是否僅僅執行基准部分的代碼,編寫一個測試套件或編碼基准庫,JavaScript基准事實上比你想象的很多其它。如需更具體的指南基准。我強烈建議你閱讀由Mathias Bynens和John-David Dalton提供的Javascript基准測試

  分析(PROFILING)

  Chrome開發人員工具為JavaScript分析有非常好的支持。能夠使用此功能檢測哪些函數占用了大部分時間。這樣你就能夠去優化它們。

這非常重要。即使是代碼非常小的改變會對總體表現產生重要的影響。

Chrome開發人員工具的分析面板

  分析過程開始獲代替碼性能基線。然后以時間線的形式體現。這將告訴我們代碼須要多長時間執行。

“Profiles”選項卡給了我們一個更好的視角來了解應用程序中發生了什么。JavaScript CPU分析文件展示了多少CPU時間被用於我們的代碼,CSS選擇器分析文件展示了多少時間花費在處理選擇器上。堆快照顯示多少內存正被用於我們的對象。

  利用這些工具。我們能夠分離、調整和又一次分析來衡量我們的功能或操作性能優化是否真的起到了效果。

“Profile”選項卡展示了代碼性能信息。

  一個非常好的分析介紹。閱讀Zack Grossbart的 JavaScript Profiling With The Chrome Developer Tools

  提示:在理想情況下,若想確保你的分析並未受到已安裝的應用程序或擴展的不論什么影響,能夠使用--user-data-dir <empty_directory>標志來啟動Chrome。在大多數情況下,這樣的方法優化測試應該是足夠的,但也須要你很多其它的時間。這是V8標志能有所幫助的。

  避免內存泄漏——3快照技術

  在谷歌內部,Chrome開發人員工具被Gmail等團隊大量使用,用來幫助發現和排除內存泄漏。

Chrome開發人員工具中的內存統計

  內存統計出我們團隊所關心的私有內存使用、JavaScript堆的大小、DOM節點數量、存儲清理、事件監聽計數器和垃圾收集器正要回收的東西。推薦閱讀Loreena Lee的“3快照”技術。該技術的要點是,在你的應用程序中記錄一些行為,強制垃圾回收。檢查DOM節點的數量有沒有恢復到預期的基線。然后分析三個堆的快照來確定是否有內存泄漏。

  單頁面應用的內存管理

  單頁面應用程序(比如AngularJS。Backbone,Ember)的內存管理是很重要的,它們差點兒永遠不會刷新頁面。

這意味着內存泄漏可能相當明顯。移動終端上的單頁面應用充滿了陷阱,由於設備的內存有限。並在長期執行Emailclient或社交網絡等應用程序。

能力愈大責任愈重。

  有非常多辦法解決問題。

在Backbone中。確保使用dispose()來處理舊視圖和引用(眼下在Backbone(Edge)中可用)。這個函數是近期加上的,移除加入到視圖“event”對象中的處理函數,以及通過傳給view的第三個參數(回調上下文)的model或collection的事件監聽器。dispose()也會被視圖的remove()調用,處理當元素被移除時的主要清理工作。Ember 等其它的庫當檢測到元素被移除時,會清理監聽器以避免內存泄漏。

  Derick Bailey的一些明智的建議:

與其了解事件與引用是怎樣工作的,不如遵循的標准規則來管理JavaScript中的內存。

假設你想載入數據到的一個存滿用戶對象的Backbone集合中。你要清空這個集合使它不再占用內存。那必須這個集合的全部引用以及集合內對象的引用。一旦清楚了所用的引用,資源就會被回收。這就是標准的JavaScript垃圾回收規則。

  在文章中,Derick涵蓋了很多使用Backbone.js時的常見內存缺陷,以及怎樣解決這些問題。

  Felix Geisendörfer的在Node中調試內存泄漏的教程也值得一讀,尤其是當它形成了更廣泛SPA堆棧的一部分。

  降低回流(REFLOWS)

  當瀏覽器又一次渲染文檔中的元素時須要 又一次計算它們的位置和幾何形狀,我們稱之為回流。回流會堵塞用戶在瀏覽器中的操作,因此理解提升回流時間是很有幫助的。

回流時間圖表

  你應該批量地觸發回流或重繪,可是要克制地使用這些方法。

盡量不處理DOM也非常重要。能夠使用DocumentFragment,一個輕量級的文檔對象。你能夠把它作為一種方法來提取文檔樹的一部分。或創建一個新的文檔“片段”。

與其不斷地加入DOM節點。不如使用文檔片段后僅僅運行一次DOM插入操作,以避免過多的回流。

  比如,我們寫一個函數給一個元素加入20個div。假設僅僅是簡單地每次append一個div到元素中。這會觸發20次回流。

function addDivs(element) {
  var div;
  for (var i = 0; i < 20; i ++) {
    div = document.createElement('div');
    div.innerHTML = 'Heya!';
    element.appendChild(div);
  }
}

  要解決問題,能夠使用DocumentFragment來取代。我們能夠每次加入一個新的div到里面。完畢后將DocumentFragment加入到DOM中僅僅會觸發一次回流。

function addDivs(element) {
  var div;
  // Creates a new empty DocumentFragment.
  var fragment = document.createDocumentFragment();
  for (var i = 0; i < 20; i ++) {
    div = document.createElement('a');
    div.innerHTML = 'Heya!';
    fragment.appendChild(div);
  }
  element.appendChild(fragment);
}

  能夠參閱 Make the Web FasterJavaScript Memory Optimization 和 Finding Memory Leaks

  JS內存泄漏探測器

  為了幫助發現JavaScript內存泄漏。谷歌的開發者((Marja Hölttä和Jochen Eisinger)開發了一種工具,它與Chrome開發者工具結合使用。檢索堆的快照並檢測出是什么對象導致了內存泄漏。

一個JavaScript內存泄漏檢測工具

  有完整的文章介紹了怎樣使用這個工具,建議你自己到內存泄漏探測器項目頁面看看。

  假設你想知道為什么這種工具還沒集成到我們的開發工具,其原因有二。

它最初是在Closure庫中幫助我們捕捉一些特定的內存場景。它更適合作為一個外部工具。

  V8優化調試和垃圾回收的標志位

  Chrome支持直接通過傳遞一些標志給V8。以獲得更具體的引擎優化輸出結果。比如,這樣能夠追蹤V8的優化:

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

  Windows用戶能夠這樣執行 chrome.exe –js-flags=”–trace-opt –trace-deopt”

  在開發應用程序時,以下的V8標志都能夠使用。

  • trace-opt —— 記錄優化函數的名稱,並顯示跳過的代碼。由於優化器不知道怎樣優化。
  • trace-deopt —— 記錄執行時將要“去優化”的代碼。

  • trace-gc —— 記錄每次的垃圾回收。

  V8的處理腳本用*(星號)標識優化過的函數,用~(波浪號)表示未優化的函數。

  假設你有興趣了解很多其它關於V8的標志和V8的內部是怎樣工作的,強烈建議 閱讀Vyacheslav Egorov的excellent post on V8 internals

  HIGH-RESOLUTION TIME 和 NAVIGATION TIMING API

  高精度時間(HRT)是一個提供不受系統時間和用戶調整影響的亞毫秒級高精度時間接口,能夠把它當做是比 new Date 和 Date.now()更精准的度量方法。

這對我們編寫基准測試幫助非常大。

高精度時間(HRT)提供了當前亞毫秒級的時間精度

  眼下HRT在Chrome(穩定版)中是以window.performance.webkitNow()方式使用,但在Chrome Canary中前綴被丟棄了,這使得它能夠通過window.performance.now()方式調用。Paul Irish在HTML5Rocks上了關於HRT很多其它內容的文章。

  如今我們知道當前的精准時間,那有能夠准確測量頁面性能的API嗎?好吧,如今有個Navigation Timing API能夠使用,這個API提供了一種簡單的方式。來獲取網頁在載入呈現給用戶時。精確和具體的時間測量記錄。能夠在console中使用window.performance.timing來獲取時間信息:

顯示在控制台中的時間信息

  我們能夠從上面的數據獲取非常多實用的信息,比如網絡延時為responseEnd – fetchStart,頁面載入時間為loadEventEnd – responseEnd,處理導航和頁面載入的時間為loadEventEnd – navigationStart。

  正如你所示,perfomance.memory的屬性也能顯示JavaScript的內存數據使用情況。如總的堆大小。

  很多其它Navigation Timing API的細節,閱讀 Sam Dutton的 Measuring Page Load Speed With Navigation Timing

  ABOUT:MEMORY 和 ABOUT:TRACING

  Chrome中的about:tracing提供了瀏覽器的性能視圖,記錄了Chrome的全部線程、tab頁和進程。

About:Tracing提供了瀏覽器的性能視圖

  這個工具的真正用處是同意你捕獲Chrome的執行數據。這樣你就能夠適當地調整JavaScript執行,或優化資源載入。

  Lilli Thompson有一篇寫給游戲開發人員的使用about:tracing分析WebGL游戲的文章,同一時候也適合JavaScript的開發人員。

  在Chrome的導航欄里能夠輸入about:memory,相同十分有用,能夠獲得每一個tab頁的內存使用情況。對定位內存泄漏非常有幫助。

  總結

  我們看到,JavaScript的世界中有非常多隱藏的陷阱,且並沒有提升性能的銀彈。

僅僅有把一些優化方案綜合使用到(現實世界)測試環境,才干獲得最大的性能收益。即便如此,了解引擎是怎樣解釋和優化代碼,能夠幫助你調整應用程序。

  測量,理解,修復。不斷反復這個過程。

圖片來源: Sally Hunter

  謹記關注優化,但為了便利能夠舍棄一些非常小的優化。比如,有些開發人員選擇.forEach和Object.keys取代for和for..in循環。雖然這會更慢但使用更方便。要保證清醒的頭腦。知道什么優化是須要的。什么優化是不須要的。

  同一時候注意,盡管JavaScript引擎越來越快。但下一個真正的瓶頸是DOM。回流和重繪的降低也是重要的。所以必要時再去動DOM。還有就是要關注網絡,HTTP請求是珍貴的。特別是移動終端上。因此要使用HTTP的緩存去降低資源的載入。

  記住這幾點能夠保證你獲取了本文的大部分信息。希望對你有所幫助!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM