【譯】編寫高性能JavaScript

原文:http://coding.smashingmagazine.com/2012/11/05/writing-fast-memory-efficient-javascript/javascript

做者:Addy Osmanicss

譯者按:本人第一次翻譯外文,言語不免有些晦澀,但儘可能表達了做者的原意,未通過多的潤色,歡迎批評指正。另本文篇幅較長、信息量大,可能難以消化,歡迎留言探討細節問題。本文主要關注V8的性能優化,部份內容並不適用於全部JS引擎。最後,轉載請註明出處: )html

========================譯文分割線===========================html5

不少JavaScript引擎,如Google的V8引擎(被Chrome和Node所用),是專門爲須要快速執行的大型JavaScript應用所設計的。若是你是一個開發者,而且關心內存使用狀況與頁面性能,你應該瞭解用戶瀏覽器中的JavaScript引擎是如何運做的。不管是V8,SpiderMonkey的(Firefox)的Carakan(Opera),Chakra(IE)或其餘引擎,這樣作能夠幫助你更好地優化你的應用程序。這並非說應該專門爲某一瀏覽器或引擎作優化,千萬別這麼作。java

可是,你應該問本身幾個問題:node

  • 在個人代碼裏,是否可使代碼更高效一些
  • 主流的JavaScript引擎都作了哪些優化
  • 什麼是引擎沒法優化的,垃圾回收器(GC)是否能回收我所指望的東西

Dashboard Speedometer

加載快速的網站就像是一輛快速的跑車,須要用到特別定製的零件. 圖片來源: dHybridcars.git

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

那麼,JavaScript在V8裏是如何工做的?

若是你對JS引擎沒有較深的瞭解,開發一個大型Web應用也沒啥問題,就比如會開車的人也只是看過引擎蓋而沒有看過車蓋內的引擎同樣。鑑於Chrome是個人瀏覽器首選,因此談一下它的JavaScript引擎。V8是由如下幾個核心部分組成:web

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

垃圾回收

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

手動消除對象引用在大多數狀況下是沒有必要的。經過簡單地把變量放在須要它們的地方(理想狀況下,儘量是局部做用域,即它們被使用的函數裏而不是函數外層),一切將運做地很好。

Garbage collection attempts to reclaim memory.

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

在JavaScript中,是不可能強制進行垃圾回收的。你不該該這麼作,由於垃圾收集過程是由運行時控制的,它知道什麼是最好的清理時機。

「消除引用」的誤解

網上有許多關於JavaScript內存回收的討論都談到delete這個關鍵字,雖然它能夠被用來刪除對象(map)中的屬性(key),但有部分開發者認爲它能夠用來強制「消除引用」。建議儘量避免使用delete,在下面的例子中delete o.的弊大於利,由於它改變了o的隱藏類,並使它成爲一個"慢對象"

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

你會很容易地在流行的JS庫中找到引用刪除——這是具備語言目的性的。這裏須要注意的是避免在運行時修改」hot」對象的結構。JavaScript引擎能夠檢測出這種「hot」的對象,並嘗試對其進行優化。若是對象在生命週期中其結構沒有較大的改變,引擎將會更容易優化對象,而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中更爲優化,可是在模塊代碼或應用中測試一下會發現,這些優化真正的效果比你指望的要小的多。

Speed trap.

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

好比咱們想要建立這樣一個模塊:

  • 須要一個本地的數據源包含數字ID
  • 繪製包含這些數據的表格
  • 添加事件處理程序,當用戶點擊的任何單元格時切換單元格的css class

這個問題有幾個不一樣的因素,雖然也很容易解決。咱們如何存儲數據,如何高效地繪製表格而且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測試確認)。

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

使用數組時的技巧

接下來講說數組相關的技巧。在通常狀況下,不要刪除數組元素,這樣將使數組過渡到較慢的內部表示。當索引變得稀疏,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引擎。

Test of empty literal versus pre-allocated arrays in various browsers.

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

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應用的世界中,速度就是一切。沒有用戶但願用一個要花幾秒鐘計算某列總數或花幾分鐘彙總信息的表格應用。這是爲何你要在代碼中壓榨每一點性能的重要緣由。

An old phone on the screen of an iPad.

圖片來源: 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分析有很好的支持。可使用此功能檢測哪些函數佔用了大部分時間,這樣你就能夠去優化它們。這很重要,即便是代碼很小的改變會對總體表現產生重要的影響。

Profiles panel in Chrome Developer Tools.

Chrome開發者工具的分析面板

分析過程開始獲取代碼性能基線,而後以時間線的形式體現。這將告訴咱們代碼須要多長時間運行。「Profiles」選項卡給了咱們一個更好的視角來了解應用程序中發生了什麼。JavaScript CPU分析文件展現了多少CPU時間被用於咱們的代碼,CSS選擇器分析文件展現了多少時間花費在處理選擇器上,堆快照顯示多少內存正被用於咱們的對象。

利用這些工具,咱們能夠分離、調整和從新分析來衡量咱們的功能或操做性能優化是否真的起到了效果。

The profile tab gives information about your code's performance.

「Profile」選項卡展現了代碼性能信息。

一個很好的分析介紹,閱讀Zack Grossbart的 JavaScript Profiling With The Chrome Developer Tools

提示:在理想狀況下,若想確保你的分析並未受到已安裝的應用程序或擴展的任何影響,可使用--user-data-dir<empty_directory>標誌來啓動Chrome。在大多數狀況下,這種方法優化測試應該是足夠的,但也須要你更多的時間。這是V8標誌能有所幫助的。

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

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

Memory statistics in Chrome Developer Tools.

Chrome開發者工具中的內存統計

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

單頁面應用的內存管理

單頁面應用程序(例如AngularJS,Backbone,Ember)的內存管理是很是重要的,它們幾乎永遠不會刷新頁面。這意味着內存泄漏可能至關明顯。移動終端上的單頁面應用充滿了陷阱,由於設備的內存有限,並在長期運行Email客戶端或社交網絡等應用程序。能力愈大責任愈重。

有不少辦法解決這個問題。在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)

當瀏覽器從新渲染文檔中的元素時須要 從新計算它們的位置和幾何形狀,咱們稱之爲迴流。迴流會阻塞用戶在瀏覽器中的操做,所以理解提高迴流時間是很是有幫助的。

Chart of reflow time.

迴流時間圖表

你應該批量地觸發迴流或重繪,可是要節制地使用這些方法。儘可能不處理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開發人員工具結合使用,檢索堆的快照並檢測出是什麼對象致使了內存泄漏。

A tool for detecting JavaScript memory leaks.

一個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()更精準的度量方法。這對咱們編寫基準測試幫助很大。

High Resolution Time (HRT) provides the current time in sub-millisecond resolution.

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

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

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

Timing information is shown in the console.

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

咱們能夠從上面的數據獲取不少有用的信息,例如網絡延時爲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 offers an intimate view of the browser’s performance.

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

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

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

在Chrome的導航欄裏能夠輸入about:memory,一樣十分實用,能夠得到每一個tab頁的內存使用狀況,對定位內存泄漏頗有幫助。

總結

咱們看到,JavaScript的世界中有不少隱藏的陷阱,且並無提高性能的銀彈。只有把一些優化方案綜合使用到(現實世界)測試環境,才能得到最大的性能收益。即使如此,瞭解引擎是如何解釋和優化代碼,能夠幫助你調整應用程序。

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

Measuring.

圖片來源: Sally Hunter

謹記關注優化,但爲了便利能夠捨棄一些很小的優化。例如,有些開發者選擇.forEach和Object.keys代替for和for..in循環,儘管這會更慢但使用更方便。要保證清醒的頭腦,知道什麼優化是須要的,什麼優化是不須要的。

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

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

相關文章
相關標籤/搜索