原文: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
加載快速的網站就像是一輛快速的跑車,須要用到特別定製的零件. 圖片來源: dHybridcars.git
編寫高性能代碼時有一些常見的陷阱,在這篇文章中,咱們將展現一些通過驗證的、更好的編寫代碼方式。github
若是你對JS引擎沒有較深的瞭解,開發一個大型Web應用也沒啥問題,就比如會開車的人也只是看過引擎蓋而沒有看過車蓋內的引擎同樣。鑑於Chrome是個人瀏覽器首選,因此談一下它的JavaScript引擎。V8是由如下幾個核心部分組成:web
垃圾回收是內存管理的一種形式,其實就是一個收集器的概念,嘗試回收再也不被使用的對象所佔用的內存。在JavaScript這種垃圾回收語言中,應用程序中仍在被引用的對象不會被清除。chrome
手動消除對象引用在大多數狀況下是沒有必要的。經過簡單地把變量放在須要它們的地方(理想狀況下,儘量是局部做用域,即它們被使用的函數裏而不是函數外層),一切將運做地很好。
垃圾回收器嘗試回收內存. 圖片來源: Valtteri Mäki.
在JavaScript中,是不可能強制進行垃圾回收的。你不該該這麼作,由於垃圾收集過程是由運行時控制的,它知道什麼是最好的清理時機。
網上有許多關於JavaScript內存回收的討論都談到delete這個關鍵字,雖然它能夠被用來刪除對象(map)中的屬性(key),但有部分開發者認爲它能夠用來強制「消除引用」。建議儘量避免使用delete,在下面的例子中delete o.x 的弊大於利,由於它改變了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着再也不使用的對象。這裏有幾件事須要記住:
接下來,咱們談談函數。正如咱們已經說過,垃圾收集的工做原理,是經過回收再也不是訪問的內存塊(對象)。爲了更好地說明這一點,這裏有一些例子。
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超出範圍)。
當你看到一個函數,返回一個內部函數,該內部函數將得到範圍外的訪問權,即便在外部函數執行以後。這是一個基本的閉包 —— 能夠在特定的上下文中設置的變量的表達式。例如:
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.
好比咱們想要建立這樣一個模塊:
這個問題有幾個不一樣的因素,雖然也很容易解決。咱們如何存儲數據,如何高效地繪製表格而且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引擎的優化點在本文討論範圍以外,固然這裏也有許多值得一提的技巧。記住這些技巧你就能減小那些性能低下的代碼了。
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也很是值得一讀。
JavaScript中對象和數組之間只有一個的主要區別,那就是數組神奇的length屬性。若是你本身來維護這個屬性,那麼V8中對象和數組的速度是同樣快的。
對於應用程序開發人員,對象克隆是一個常見的問題。雖然各類基準測試能夠證實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