許多Javascript引擎都是爲了快速運行大型的JavaScript程序而特別設 計的,例如Google的V8引擎(Chrome瀏覽器,Node均使用該引擎)。在開發過程當中,若是你關心你程序的內存和性能的話,你應該瞭解並意識 到,在你的代碼背後,瀏覽器的JavaScript引擎中到底發生了什麼事情。javascript
不論的V8,SpiderMonkey(Firefox)、Carakan(Opera)、Chakra(IE)或者其它類型的引擎。瞭解引擎背後 的一些運行機制能夠幫助你更好的優化你的應用程序。這並非意味着你只爲一種瀏覽器或者一種引擎進行程序的優化,並且,永遠不要這樣作。html
然而,你應該問本身下面這些問題:html5
在咱們編寫高效、快速的代碼的時候,有許多常見的陷阱。在這篇文章當中,咱們會去探索一些方法,讓你的代碼擁有更加良好的性能,咱們也會爲這些代碼提供測試樣例。java
雖然在沒有完全瞭解JavaScript引擎的狀況下,開發出大型的應用程序是有可能的,這就像車主開過車卻沒有看過引擎蓋背後的東西同樣。把我選擇的Chrome瀏覽器做爲例子,我將會談談它的JavaScript引擎的工做機制。V8引擎,是由幾個核心的部分組成的。node
● 一個基本的編譯器(basecompiler),在你的代碼運行以前,它會分析你的JavaScript代碼而且生成本地的機器碼,而不是經過字節碼的方式來運行,也不是簡單地解釋它。這種機器碼起初是沒有被高度優化的。git
● V8經過對象模型(objectmodel)來表達你的對象。對象是在JavaScript中是以關聯數組的方式呈現的,可是在V8引擎中,它們是經過隱藏類(hiddenclasses)的方式來表示的。這是一種能夠優化查找的內部類型機制(internaltypesystem)。程序員
● 一個運行期剖析器(runtimeprofiler),它會監視正在運行的系統,而且標識出「熱點」函數(「hot」function),也就是那些最後會花費大量運行時間的代碼。github
● 一個優化編譯器(optimizingcompiler),從新編譯並優化運行期剖析器所標識「熱點」代碼,而後執行優化,例如,把代碼進行內聯化(inlining)(也就是在函數被調用的地方用函數主體去取代)。web
● V8引擎支持逆優化(deoptimization),意味着若是優化編譯器發如今某些假定的狀況下,把一些已經優化的代碼進行了過分的優化,它就會把它門從生成的代碼中抽離出來。chrome
● V8擁有垃圾回收器。理解它是如何運做的和理解如何優化你的JavaScript代碼同等重要。
垃圾回收
垃圾回收是一種內存管理機制。垃圾回收器的概念是,它會嘗試去從新分配已經不須要的對象所佔據的內存空間。在如JavaScript擁有垃圾回收機制的語言中,若是你的程序中仍然存在指向一個對象的引用,那麼該對象將不會被回收。
在大多數的狀況下,咱們沒有必要去手動得解除對象的引用(de-referencing)。只要簡單地把變量放在它們應該的地方(在理想的狀況下,變量應該儘可能爲局部變量,也就是說,在它們被使用的函數中聲明它們,而不是在更外層的做用域),垃圾就能正確地被回收。
在JavaScript中強制進行垃圾回收是一件不可能的事情,並且你也不會想這樣作。由於垃圾回收的過程是由運行期所控制的,回收器一般知道垃圾回收的最佳時機在何時。
關於解除引用的誤解
在網上很多關於JavaScript的內存分配問題的討論中,關鍵字delete被頻繁提出。雖然它本意是用來刪除映射(map)中的鍵 (keys),可是很多的開發者認爲也可使用它來強制解除引用。在可能的狀況下,儘可能避免使用delete。在下面的例子中,刪除o.x在的代碼背後會 發生一些弊大於利的事情,由於它會改變o的隱藏類,而且把它轉化成通常的對象,而這些通常對象會更慢。
1
2
3
|
var o = { x: 1 };
delete o.x; // true
o.x; // undefined
|
也就是說,在如今流行的JavaScript庫中,你幾乎確定能找到delete刪除引用的身影——它也確實存在這個語言目的。這裏提出來的主旨 是,讓你們儘可能避免在運行期改變熱點對象(hotobjects)的結構。JavaScript引擎能夠檢測出這種的「熱點」對象並嘗試去優化它們,若是 在對象的生命期中沒有遇到重大的結構改變,引擎的檢測和優化過程會來得更加容易,而使用delete則會觸發對象結構上的這種改變。
很多人對null的使用上也存在誤解。將一個對象的引用設爲null,並非意味着「清空」該對象,而是將該引用指向null。用o.x=null比用delete要好,但這甚至可能不是必要的。
1
2
3
4
|
var o = { x: 1 };
o = null;
o; // null
o.x // TypeError
|
若是被刪除的引用是指向對象的最後一個引用,那麼該對象就知足了垃圾回收的資格。若是該引用不是指向對象的最後一個引用,那麼該對象仍然能夠被獲取,而不會被垃圾回收。
另外要重點注意的是,要意識到,在你頁面的生命期中,全局變量不會被垃圾回收器所清理。只要你的頁面保持打開狀態,JavaScript運行期中的全局對象就會常駐在內存當中。
1
|
var myGlobalNamespace = {};
|
只有當你刷新頁面,導航到不一樣的頁面,關閉選項卡,或關閉你的瀏覽器,全局變量纔會被清理。當函數做用域變量超出做用域範圍,它就會被清理。當函數徹底結束,而且再沒有任何引用指向其中的變量,函數中的變量會被清理。
經驗法則
爲了給垃圾回收器儘早,儘可能多地回收對象的機會,不要保留你再也不須要的對像。這種狀況大多會自動發生;這裏有幾件事是要謹記的:
● 就像以前所說的那樣,一個比手動解除引用更好的選擇是,在恰當的做用域中使用變量。也就是說,用能夠自動從做用域中剔除的函數局部變量,去取代要手動清空的全局變量。這意味着你的代碼會更加的整潔且要擔心的事情會更少。
● 確保要及時註銷掉你再也不須要的監聽事件。特別是對那些必然要刪除的DOM對象。
● 若是你正在使用本地數據緩存的話,確保要清除數據緩存或者使用老化機制(agingmechanism),以避免保存了大量你不大可能複用的數據。
函數
接下來,讓咱們看看函數。正如咱們所說的,垃圾回收是經過從新分配已經沒法經過引用得到的內存塊(對象)來工做的。爲了更好地說明這一點,這裏有一些例子。
1
2
3
4
|
function foo() {
var bar = new LargeObject();
bar.someCall();
}
|
當foo函數結束的時候,bar指向的對象就會自動地被垃圾回器所獲取,由於已經沒有任何引用指向該對象了。
對比如下代碼:
1
2
3
4
5
6
7
8
|
function foo() {
var bar = new LargeObject();
bar.someCall();
return bar;
}
// somewhere else
var b = foo();
|
如今咱們有了一個指向該對象的引用,這個引用會在該次調用中保留下來,直到調用者將b賦值給其餘東西(或者b超出了做用域範圍)。
閉包
如今咱們來看看一個返回內部函數的函數,那個內部函數能夠訪問到更外層的做用域,即便外部函數已經執行完畢。這基本上就是一個閉包——一種可使用設置在特殊上下文中的變量的表現。例如:
1
2
3
4
5
6
7
8
9
10
11
|
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運行上下文中創造的函數對象不會被垃圾回收,由於它被一個全局變量所指向,仍然很是容易被訪問到。它能夠經過sumA(n)來運行。
讓咱們來看另一個例子。這裏,咱們能夠訪問到largeStr嗎?
1
2
3
4
5
6
|
var a = function () {
var largeStr = new Array(1000000).join('x');
return function () {
return largeStr;
};
}();
|
答案是確定的,咱們能夠經過a()來訪問到它,因此它不會被回收。咱們看看這個會怎麼樣:
1
2
3
4
5
6
7
|
var a = function () {
var smallStr = 'x';
var largeStr = new Array(1000000).join('x');
return function (n) {
return smallStr;
};
}();
|
咱們不再能訪問到它了,它會成爲垃圾回收的候選對象。
定時器
最糟糕的情況之一是內存在循環中,或者在setTimeout()/setInterval()中泄露,但這至關的常見。
考慮下面的例子:
1
2
3
4
5
6
7
8
9
|
var myObj = {
callMeMaybe: function () {
var myRef = this;
var val = setTimeout(function () {
console.log('Time is running out!');
myRef.callMeMaybe();
}, 1000);
}
};
|
若是咱們這樣運行:
1
|
myObj.callMeMaybe();
|
開始定時器,咱們會看到每秒鐘顯示「Timeisrunningout!」而後若是咱們運行下面代碼:
1
|
myObj = null;
|
定時器仍然運做。myObj不會被垃圾回收,由於傳入setTimout的閉包函數仍然須要它來保證正常運做。反過來,閉包函數保留了指向 myObj的引用,由於它經過myRef來獲取了該對象。若是咱們把該閉包函數傳入其餘任何的函數,一樣的事情同樣會發生,函數中仍然會存在指向對象的引 用。
一樣值得緊緊記住的是,在setTimeout/setInterval的調用中的引用,例如函數引用,在運行完成以前是不會被垃圾回收的。
注意性能陷阱
很重要的一點是,除非你真正須要,不然沒有必要優化你的代碼,這個怎麼強調都不爲過。在大量的微基準測試中,你能夠很輕易地發現,在V8引擎中N比M更加的優化,可是若是在真實的代碼模型或者在真正的應用程序中進行測試,那些優化的實際影響可能比你指望的要小得多。
假設如今咱們想要創建的一個模塊:
● 經過數字ID取出本地存儲的數據資源。
● 用得到的數據生成表格內容。
● 爲每一個表格單元添加事件處理,每當用戶點擊表格單元,切換表格單元的class。
即便這個問題解決起來很直觀,可是有一些困難的因素。咱們如何去存儲這些數據,如何能夠高效地生成一個表格並把它添加到DOM中去,如何優化地處理這個表格的事件處理?
第一個(也是幼稚的)採起的方案多是將每塊可獲取的數據存放在一個對象中,而後把全部對象集合到一個數組當中。有的人可能會用jQuery去循環訪問數據而後把生成表格內容,而後把它添加到DOM中。最後,有的人可能會用使用事件綁定添加點擊咱們須要的點擊事件。
注意:這不是你應該作的事情:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
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(上面的jQuery用法)更加的優化。固然,使用事件委託一般比 爲每一個td都進行事件綁定會有更好的性能。
注意jQuery內部確實使用DocumentFragment進行了優化,但在咱們的例子中,代碼中在循環中調用append(),每一次調用都 要進行額外的操做,因此在這個例子中,它達到優化效果可能並不大。但願這應該不會是一個痛處,可是必定要用基準測試來確保本身的代碼沒有問題。
在咱們的例子當中,添加這些以上的優化會獲得一些不錯(預期)的性能收益。相對於簡單的綁定,事件委託提供了至關好的改進,且選擇用documentFragment會是一個真正的性能助推器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
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模板框架是通過高度的優化的。有時它們的確是這樣,可是使用它們只是爲了代碼擁有更強的可讀性。同時,還有預編譯!讓咱們測試一下, 實際上這有多少是能帶來真正優化的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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上繪製圖像,和如何使用或不使用類型數組去操做像素數據。
在你的代碼使用它們以前,要給你的微基準測試一個結束前的檢驗。大家其中有些人可能會回想起JavaScript模板語言shoot-off和它的 以後擴展版的shoot-off。若是你想確保測試不會被現實的應用程序的中你不想見到的約束所影響——請在真實的代碼中和優化一塊兒測試。
V8優化技巧
同時詳細的陳列每個V8的每一種優化顯然超出了本文的討論範圍,其中有許多特定的優化技巧都值得注意。記住如下的一些建議你就能夠減小你寫出低性能的代碼的機會。
● 特定的模式會致使V8放棄優化。例如使用try-catch,就會致使這種狀況的發生。若是想要了解跟多關於什麼函數能夠被優化,什麼函數不能夠,你可使用V8引擎中附帶的D8shell實用程序中的–trace-optfile.js。
● 若是你關心運行速度,那麼就要儘可能保持你的函數的功能的單一性,也就是說,確保變量(包括屬性,數組,和函數參數)永遠只是相同隱藏類的包含對象。例如,永遠不要幹這種事:
● 不要從未初始化的或已經被刪除的元素上加載內容。這樣作可能對你的程序運行結果不會形成影響。可是它會使得程序運行得更慢。
● 不要寫過於龐大的函數,由於他們更難被優化。
若是想知道更多的優化技巧,能夠觀看DanielClifford的GoogleI/O大會上的演講BreakingtheJavaScriptSpeedLimitwithV8,它同時也涵蓋了上面咱們所說的優化技巧。OptimizingForV8—ASeries也一樣值得一讀。
對象和數組:我應該用哪個?
● 若是你想存儲一組數字,或者一系列的同類型對象的話,那麼就使用數組。
● 若是你想要的是一個語義上的有不一樣屬性(不一樣類型)的對象,那麼就使用包含屬性的對象。這樣從內存上來講會至關的高效,並且運行也至關的迅速。
● 用整數作索引的元素,無論它們是存儲在數組仍是對象中,都會比那些需經過迭代來獲取的對象屬性要快得多。
● 對象中的屬性至關複雜:它們能夠被setter所建立,擁有不一樣的可枚舉性和可寫性。數組中的元素不能有這樣的定製性——它們只有存在或者不存在的狀態。 在一個引擎的層面,從組織表示結構的內存角度上來講,這容許有更多的優化。當一個數組中包含有數字的時候,這樣會至關有好處。例如,當你須要一個向量,不 要用一個包含有x,y,z屬性的對象,用一個數組來存儲就能夠了。
使用對象的技巧
● 用構造函數構造對象。這樣能夠保證全部的由該構造函數構造的對象都具備相同的隱藏類,並且能夠有助於避免修改這些隱藏類。有個附加的好處就是,它會比Object.create()稍快。
● 在你的程序中,對象的類型數目以及它們的複雜程度是沒有限制的(不難理解的是:長原型鏈會可能會致使有害的結果,那些只有少數屬性的對象的特殊表現就是,它們會比那些更大的對象運行得要快一點)。對於「熱點」對象,儘可能保持原型鏈的簡短,以及屬性數目較少。
對象的複製
對於應用的開發者來講,對象的複製是一個常見的問題。雖然基礎測試可能代表V8在不一樣的狀況下對這類問題都處理得很好,可是,當你要複製任何東西的 時候,仍然須要當心。複製大的東西一般是緩慢的——因此不要這樣作。JavaScript中的for…in循環處理這種事情特別的糟糕,由於它擁有可怕的 規範,這使得它在任何引擎中處理任何對象,都不會得到良好的速度。
當你必定要在一段性能要求苛刻的代碼中複製對象(而且你沒法擺脫這種情況),那麼就用數組或者一個自定義的「拷貝構造函數」,幫你逐一明確地複製對象的每個屬性。這多是實現的最快的方式:
1
2
3
4
5
|
function clone(original) {
this.foo = original.foo;
this.bar = original.bar;
}
var copy = new clone(original);
|
模塊模式中的緩存函數
在模塊模式中緩存你的函數能夠帶來性能的提升。看看下面的例子,由於它老是會強制進行成員函數的複製,你習慣看到的變化可能會更慢。
這裏有個關於原型對比模塊模式的性能測試。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
// 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一些暗示,讓它知道數組的大小和類型。字面量一般對規模不大的數組是好處的。
1
2
3
4
5
6
7
8
|
// 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),歷來不是一個好主意(也就是像這樣,vararr=[1,「1」,undefined,「true」])。
咱們能夠從結果中看出,ints數組是最快的。
稀疏數組VS滿數組
當你使用稀疏數組的時候,要意識到,在它們中訪問元素的效率要比在滿數組中要慢得多。這是由於若是數組中只有少數元素,V8不會爲元素重新分配連續的內存空間。它們會被一個字典所管理,這樣能夠節約內存空間,可是會消耗訪問時間。
滿數組的加法和無0的數組的加法其實是最快的。而一個滿數組中是否含有0對它的運行效率沒有影響。
塞滿的數組VS多孔的數組
避免數組中的「孔」(可能經過刪除元素或者用a[x]=foo,而x>a.length來建立的孔)。在一個「滿」的數組中,即便是僅僅是一個元素被刪除掉,也會變得慢得多。
預分配數組VS運行時分配
不要根據數組最大的大小預分配一個大數組(例如大於64K的元素),應該讓你的數組在運行的時候自我分配。在咱們進行對這個技巧的性能測試以前,請記住,這隻適合部分JavaScript瀏覽器。
在Nitro引擎(Safari)使用預分配的數組會更有好處。可是,在其餘的引擎中(V8,SpiderMonkey),非預分配會更高效。
1
2
3
4
5
6
7
8
9
10
11
|
// 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應用程序的世界裏面,速度是一切。沒有用戶但願一個電子表格程序須要幾秒鐘的時間去計算一列數據的和,或者用一分鐘的時間去獲得表格的彙總信息。這就爲何你須要壓榨你代碼中的每一點的性能的緣由,這有時可能會苛刻。
雖然理解和提升你的應用程序性能是有用的,可是它依然很困難。咱們給出下面幾步建議去解決你程序性能的瓶頸:
● 測量它:找出你應用程序中慢的地方(~45%)
● 理解它:發現實際的問題是什麼(~45%)
● 解決它!(~10%)
一些推薦的工具和技術能夠協助你進行這個過程。
基準測試
有許多方法能夠測試JavaScript代碼片段的性能——通常的設想是:基準測試就是簡單地對兩個時間戳進行比較。這樣的模式已經被jsPerf團隊所指出,而且應用在在SunSpider和Kraken的基準測試套件當中。
1
2
3
4
5
6
7
8
9
|
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;
|
這裏,測試代碼被放在一個循環當中,而且運行一個設定的次數(例如六次)。接下來,用結束的時間去減開始開始的時間。這樣,就能夠測試出循環中操做所消耗的時間。
然而,這裏對基準測試的工做過分簡化了,尤爲是若是你想在多個瀏覽器和環境中進行基準測試。垃圾回收自己會影響到你的測試結果。即便你使用了像window.preformance這樣的解決方案,你仍然須要對那些陷阱作出相應的考慮。
無論你是簡單地對你部分的代碼進行基準測試,仍是編寫一個測試套件,或寫一個基準測試的類庫。關於JavaScript基準測試實際要作的事情比你 想象的要多。若是你想得到關於基準測試更多的細節指導,我強烈建議你閱讀MathiasBynens和John-DavidDalton編的JavaScriptBenchmarking
性能分析
Chrome的開發者工具提供了對JavaScript性能分析良好的支持。你可使用這些特性去檢測那些函數消耗了你大部分的性能,而後你就能夠對它們進行優化。這是很重要的一點,由於即便你的代碼庫中一點很小的改變均可以影響到你的總體性能。
性能分析以獲取你代碼當前性能的基線開始,你能夠從時間軸上發現它。它會告訴你你的代碼花費了多長的運行時間。Profiles選項卡提供了一個更 好的視角去觀察咱們的應用程序內部到底放生了什麼事情。JavaScriptCPUprofile展現了咱們的代碼到底佔用了多少CPU的時 間,CSSselectorprofile告訴咱們選擇器查找元素所花費的時間,Heapsnapsshots讓咱們知道咱們的對象佔用了多少內存。
使用這些工具,咱們能夠抽離,調整和從新分析來度量咱們的對特定函數或操做的改變是否真正起到了性能優化的效果。
想獲得關於性能分析更好的介紹,能夠閱讀ZackGrossbart的JavaScriptProfilingWithTheChromeDeveloperTools,
提示:在理想狀況下,若是你想保證的你性能分析沒有受到你所安裝的擴展程序或者應用所影響。以能夠用usingthe–user-data- dir<empty_directory>的flag來運行Chrome。在大多數狀況下,這樣的性能測試已經足夠了,可是有些時候你會須要 的更多。V8的flags就能夠爲你提供幫助。
避免內存泄露——用三快照(THREESNAPSHOT)技術發現問題
在Google內部,Chrome的開發者工具被例如Gmail這樣的團隊大量使用,能夠幫助咱們發現並解決內存泄露問題。
咱們團隊關注的一些內存數據,包括私有內存使用,JavaScript堆大小,DOM節點數目,內存清理,事件監聽數目和垃圾回收器的運行狀況。如 果你對事件驅動架構比較熟悉,你或許比較有興趣瞭解最多見問題之一就是,咱們過去經常用listen(),unlisten()(閉包),和缺失的 dispose()去處理事件監聽對象。
幸運的是,DevTools能夠幫咱們解決其中的一些問題,強烈建議閱讀LoreenaLee的很棒的一個展現,它記錄瞭如何使用「三快照技術」找出DevTools中的內存泄露。
這項技術的要點是,記錄你應用程序中的一些行爲,強制進行垃圾回收,檢測DOM的節點數目是否會返回到你所指望的基線,而後分析三個堆上的快照來決定內存是否泄露。
單頁面程序的內存管理
內存管理對於現代的單頁面應用程序(例如AngularJS,Backbone,Ember)至關的重要,由於它們幾乎歷來不會刷新頁面。這就意味 着內存泄露會至關的明顯和迅速。這在移動單頁面應用程序中存在至關大的陷阱,由於內存的限制,且存在大量的例如email客戶端這樣的長期運行的程序或者 社交網絡應用程序。能力越大,責任越大。(推薦閱讀:《每位開發人員都應銘記的10句編程諺語》)
有許多方法能夠避免這中狀況。在Backbone中,確保你老是用dispose()(當前能夠在Backbone(edge)中使用)來處理了舊 的視圖和引用。這個函數是最近新增的,能夠移除任何添加到視圖的「event」對象的全部處理函數,以及視圖做爲第三個參數(回調上下文)傳入的任何集合 或者模型的事件監聽器,dispose()一樣能夠被視圖的remove()所調用,當元素從頁面中被移除時管理主要的基本內存清理工做。其餘的類庫,如 Ember,當它檢測到元素已經從視圖中被移除的時候,它會移除相應的觀察者,以免內存泄露。
DerickBailey給了咱們一些明智的建議:
「除了瞭解事件處理在引用層面是如何工做的,在JavaScript按照標準規則來管理你的內存,一切就會沒問題。若是你想向一個塞滿用戶數據的 Backbone集合中加載數據,若是你想那些集合稍後被清理並不佔據內存,你必須移除集合全部的引用以及其中獨立的對象。一旦你移除全部的引用,清理工 做就能夠進行。這就是標準的JavaScript垃圾回收規則」
在這篇文章中,Derick涵蓋了在使用Backbone.js中許多常見的內存陷阱以及解決方案。
這裏也有一篇由FelixGeisendrfer編寫關於如何調試Node中的內存泄露的指導,很值得一讀,尤爲它造成了你的更普遍SPA堆裏面的一部分。
最小化重排
當瀏覽器爲了重繪(re-rendering)必須從新計算元素的位置和幾何形狀的時候,咱們把這個過程稱做重排(reflow)。在瀏覽器中,重排是一個用戶阻塞的操做,因此瞭解如何去提升重排的性能會頗有幫助。
你應該用批量處理的方法去觸發重排或重繪,而且要有節制地使用這些方法。儘可能不進行DOM操做也很重要。用輕量級的DocumentFragment(文本片斷)對象來達到這樣的效果。你能夠把它當作是獲取部分DOM樹的方法,或者是建立新的文本「片斷」的方法。對比不斷地向DOM樹添加節點,咱們使用文本片斷構建起咱們須要的內容而且只進行一次DOM插入操做。這樣能夠避免過分的重排。
例如,咱們寫了一個向一個元素中添加20個div函數。簡單地添加每一個div到元素中會觸發20次重排。
1
2
3
4
5
6
7
8
|
function addDivs(element) {
var div;
for (var i = 0; i < 20; i ++) {
div = document.createElement('div');
div.innerHTML = 'Heya!';
element.appendChild(div);
}
}
|
爲了解決這個問題,咱們使用DocumentFragment來代替逐個添加div。使用咱們像appendChild這樣的方法把DocumentFragment添加到元素中的時候,文本片斷中全部的子節點都會被添加到元素中,這樣只會觸發僅僅一次重排。
1
2
3
4
5
6
7
8
9
10
11
|
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);
}
|
你能夠在MaketheWebFaster,JavaScriptMemoryOptimization和FindingMemoryLeaks.閱讀到更多關於這方面的主題。
JavaScript內存泄露檢測器
爲了幫助發現JavaScript內存泄露,我兩個谷歌的同事(MarjaHlttandJochenEisinger)開發了一個和Chrome開發者工具共同使用的工具(具體來講,就是一個遠程檢測協議),能夠檢索堆快照和探測出哪些對象致使了內存泄露。
有一篇文章完整地介紹了怎麼使用這個工具,我鼓勵你去看看或者去看LeakFinder的項目主頁。
更多信息:也許你想知道爲何這個工具沒有集成到咱們的開發者工具當中。有兩個緣由,第一,它最初是爲了幫助咱們在Closure庫中爲咱們檢測特 定的內存場景。第二,它做爲一個擴展工具來使用將會更有意義(甚至能夠做爲一個擴展程序,若是咱們能夠適當地得到堆性能分析擴展程序的API的話)。
V8標籤:調試性能&垃圾回收
Chrome支持經過js-flags直接傳入一些標籤到V8引擎中來獲取關於引擎優化過程當中更多細節。例如,這能夠追溯V8的優化:
1
|
"/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的tick-processing腳本用*標註已經進行過優化的函數,用~標記沒有被優化的函數。
若是你想了解更多關於V8引擎標籤以及V8內部工做原理的話,我強烈建議你瀏覽一篇關於V8引擎內部工做原理的文章,它總結了一些目前爲止最好的資源。
高精度時間以及導航計時API
高精度時間(HRT)是一個不受系統時鐘以及用戶調整影響的亞毫秒級的JavaScript接口。它提供了比newDate和Date.now()更爲精準的時間測量。這樣能夠幫助咱們寫出性能良好的基礎測試。
HRT目前在Chrome(穩定版)中能夠經過window.performance.webkitNow()來得到,可是前綴在 ChromCanary中被省略了,能夠經過window.performance.now()來獲取。PaulIrish在HTML5Rocks中寫了 一篇關於HRT的文章。
因此,咱們如今知道了目前的時間,可是若是咱們須要API給出更精確的時間去測量web中的性能呢?
如今,咱們有個NavigationTimingAPI的東西可使用。這個API提供了一個簡單的方法去獲取當頁面加載完畢並展現給用戶時的精確和詳細的時間測量。時間信息能夠經過window.performance.timing暴露出來,你能夠在控制檯中簡單地使用它:
觀察上面的數據,咱們能夠抽離出一些至關有用的信息。例如,網絡延遲爲responseEnd-fetchStart,從服務器加載頁面時間爲 loadEventEnd-responseEnd,以及導航和頁面加載之間的的耗時爲loadEventEnd-navigationStart。
正如你所看到的,一個performance.memory屬性一樣能夠提供例如總堆大小的JavaScript內存使用狀況。
關於導航計時API更多的細節,你能夠閱讀SamDutton的一篇至關好的文章MeasuringPageLoadSpeedWithNavigationTiming.
ABBOUT:MEMORY和ABOUT:TRACING
Chrome中的about:tracing提供了有效的視圖,幫助咱們觀察瀏覽器的性能,記錄Chrome如每一個線程,選項卡,和進程的全部活動。
這個工具真正的有用的地方是能夠容許你獲取Chrome的瀏覽器引擎蓋下的分析數據,而後你能夠恰當地調整你的JavaScript程序,或者優化你資源加載過程。
LilliThompson有一篇寫給遊戲開發者的文章,關於如何使用about:tracing去分析WebGL遊戲。這篇文章對於普通的JavaScripters依然適用。
Chrome中使用about:memory也頗有幫助,由於它顯示了每一個選項卡精確的內存使用,這樣能夠有效的跟蹤潛在的內存泄露。
總結
正如咱們所見,在JavaScript的引擎世界裏面,有許多的隱藏的性能陷阱,事實上並無性能提升的銀彈。只有當你在(現實世界的)測試環境中結合一系列的優化,你纔會意識到最大的性能獲益。可是即便這樣,理解引擎內部原理以及優化你的代碼能夠給你提升你應用程序性能的靈感。
測量它,理解它,解決它。不斷重複這個過程。
記得關注優化,但要避免一些小的優化從而得到更大的便利。例如,一些開發者在循環中爲便利而是用.forEach和Object.keys來代替 for和forin,即便它們更慢,可是在能夠接受範圍內,它們更爲方便。你須要一個清醒的頭腦去分析你的應用程序中哪些是須要優化的,哪些是不須要的。
一樣,意識到即便JavaScript引擎不斷變得更快,一個真證的瓶頸實際上是DOM。重排和重繪的最小化是至關重要的,因此記住若是不是非不得已,不要觸碰DOM。而且關注網絡狀況。HTTP請求是珍貴的,尤爲是在移動終端,你應該使用HTTP緩存來減小資源的消耗。
記住全部這一切能夠確保你已經得到這篇文章的大部分信息。我但願這會對你有所幫助!