如今主流瀏覽器都在比拼JavaScript引擎的執行速度,但最終都會達到一個理論極限,即無限接近編譯后程序執行速度。 這種情況下決定程序速度的另一個重要因素就是代碼本身。
在這里我們會分門別類的介紹JavaScript性能優化的技巧,並提供相應的測試用例,供大家在自己使用的瀏覽器上驗證, 同時會對特定的JavaScript背景知識做一定的介紹。
目錄
變量查找優化
變量聲明帶上var
1. 如果聲明變量忘記了var,那么js引擎將會遍歷整個作用域查找這個變量,結果不管找到與否,都是悲劇。
- 如果在上級作用域找到了這個變量,上級作用域變量的內容將被無聲的改寫,導致莫名奇妙的錯誤發生。
- 如果在上級作用域沒有找到該變量,這個變量將自動被聲明為全局變量,然而卻都找不到這個全局變量的定義。
2. 基於上面邏輯,性能方面不帶var聲明變量自然要比帶var速度慢
具體可以參考http://jsperf.com/withvar-withoutvar。下面是個簡單的結果截圖,藍色為帶var的情況,越長說明 速度越快。
慎用全局變量
1. 全局變量需要搜索更長的作用域鏈。
2. 全局變量的生命周期比局部變量長,不利於內存釋放。
3. 過多的全局變量容易造成混淆,增大產生bug的可能性。
全局變量與局部變量的測試可以參考http://jsperf.com/local-global-var
以上兩條還可以得出一條JavaScript常用的編程風格,具有相同作用域變量通過一個var聲明 。
這樣方便查看該作用域所有的變量,JQuery源代碼中就是用了這種風格。例如下面源代碼
https://github.com/jquery/jquery/blob/master/src/core.js
jQuery.extend = jQuery.fn.extend = function() {
var options, name, src, copy, copyIsArray, clone,target = arguments[0] || {},i = 1,length =
arguments.length,deep = false;
緩存重復使用的全局變量
1. 全局變量要比局部變量需要搜索的作用域長
2. 重復調用的方法也可以通過局部緩存來提速
3. 該項優化在IE上體現比較明顯
緩存與不緩存變量的測試可以參考http://jsperf.com/localvarcache
JQuery源代碼中也是用了類似的方法,https://github.com/jquery/jquery/blob/master/src/selector-native.js
var docElem = window.document.documentElement, selector_hasDuplicate,
matches = docElem.webkitMatchesSelector || docElem.mozMatchesSelector || docElem.oMatchesSelector ||
docElem.msMatchesSelector,
selector_sortOrder = function( a, b ) {
// Flag for duplicate removal
if ( a === b ) {
selector_hasDuplicate = true;
return 0;
}
避免使用with
with語句將一個新的可變對象推入作用域鏈的頭部,函數的所有局部變量現在處於第二個作用域鏈對象中,從而使局部變 量的訪問代價提高。
var person = {
name: “Nicholas",
age: 30
}
function displayInfo() {
var count = 5;
with (person) {
alert(name + ' is ' + age);
alert('count is ' + count);
}
}
以上代碼的結果將name和age兩個變量推入第一個作用域,如下圖所示,
使用with與不使用with的測試可以參考http://jsperf.com/with-with
核心語法優化
通過原型優化方法定義
1. 如果一個方法類型將被頻繁構造,通過方法原型從外面定義附加方法,從而避免方法的重復定義。
2. 可以通過外 部原型的構造方式初始化值類型的變量定義。(這里強調值類型的原因是,引用類型如果在原型中定義, 一個實例對引用類型的更改會影響到其他實例。)
這條規則中涉及到JavaScript中原型的概念,
- 構造函數都有一個prototype屬性,指向另一個對象。這個對象的所有屬性和方法,都會被構造函數的實例繼承。我們可 以把那些不變的屬性和方法,直接定義在prototype對象上。
- 可以通過對象實例訪問保存在原型中的值,不能通過對象實例重寫原型中的值。
- 在實例中添加一個與實例原型同名屬性,那該屬性就會屏蔽原型中的屬性。
- 通過delete操作符可以刪除實例中的屬性。
例如以下代碼以及相應的內存中原型表示如下,
function Person(){}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //”Nicholas”
var person2 = new Person();
person2.sayName(); //”Nicholas”
原型附加方法測試可以參考http://jsperf.com/func-constructor
原型附加值類型變量測試可以參考http://jsperf.com/prototype2
避開閉包陷阱
1. 閉包是個強大的工具,但同時也是性能問題的主要誘因之一。不合理的使用閉包會導致內存泄漏。
2. 閉包的性能不如使用內部方法,更不如重用外部方法。
由於IE瀏覽器的DOM是用COM來實現的, COM的內存管理是通過引用計數的方式,引用計數有個難題就是循環引用,一旦DOM 引用了閉包(例如event handler),閉包的上層元素又引用了這個DOM,就會造成循環引用從而導致內存泄漏。

關於Js內存泄漏可以參考
http://www.crockford.com/javascript/memory/leak.html
http://msdn.microsoft.com/en-us/library/bb250448%28v=vs.85%29.aspx
閉包與非閉包的測試http://jsperf.com/closure2
避免使用屬性訪問方法
1. JavaScript不需要屬性訪問方法,因為所有的屬性都是外部可見的。
2. 添加屬性訪問方法只是增加了一層重定向 ,對於訪問控制沒有意義。
使用屬性訪問方法示例
function Car() {
this.m_tireSize = 17;
this.m_maxSpeed = 250;
this.GetTireSize = Car_get_tireSize;
this.SetTireSize = Car_put_tireSize;
}
function Car_get_tireSize() {
return this.m_tireSize;
}
function Car_put_tireSize(value) {
this.m_tireSize = value;
}
var ooCar = new Car();
var iTireSize = ooCar.GetTireSize();
ooCar.SetTireSize(iTireSize + 1);
直接訪問屬性示例
function Car() {
this.m_tireSize = 17;
this.m_maxSpeed = 250;
}
var perfCar = new Car();
var iTireSize = perfCar.m_tireSize;
perfCar.m_tireSize = iTireSize + 1;
使用屬性訪問與不使用屬性訪問的測試http://jsperf.com/property-accessor
避免在循環中使用try-catch
1. try-catch-finally語句在catch語句被執行的過程中會動態構造變量插入到當前域中,對性能有一定影響。
2. 如 果需要異常處理機制,可以將其放在循環外層使用。
循環中使用try-catch
for (var i = 0; i < 200; i++) {
try {} catch (e) {}
}
循環外使用try-catch
try {
for (var i = 0; i < 200; i++) {}
} catch (e) {}
循環內與循環外使用try-catch的測試http://jsperf.com/try-catch
使用for代替for…in…遍歷數組
for…in…內部實現是構造一個所有元素的列表,包括array繼承的屬性,然后再開始循環。相對for循環性能要慢。
StackOverflow上對這個for和for in的問題有個經典的回答,直接原文引用,
Q: I've been told not to use "for...in" with arrays in JavaScript. Why not?
A: The reason is that one construct...
var a = [];
a[5] = 5; // Perfectly legal JavaScript that resizes the array.
for (var i=0; i<a.length; i++) {
// Iterates over numeric indexes from 0 to 5, as everyone expects.
}
can sometimes be totally different from the other...
var a = [];
a[5] = 5;
for (var x in a) {
// Shows only the explicitly set index of "5", and ignores 0-4
}
Also consider that JavaScript libraries might do things like this, which will affect any array you create:
// Somewhere deep in
your JavaScript library...
Array.prototype.foo = 1;
// Now you have no idea what the below code will do.
var a = [1,2,3,4,5];
for (var x in a){
// Now foo is a part of EVERY array and
// will show up here as a value of 'x'.
}
關於for和for…in…的測試可以看http://jsperf.com/forin
使用原始操作代替方法調用
方法調用一般封裝了原始操作,在性能要求高的邏輯中,可以使用原始操作代替方法調用來提高性能。
原始操作
var min = a < b ? a : b;
方法實例
var min = Math.min(a, b);
關於方法調用和原始操作的測試參考http://jsperf.com/operator-function
傳遞方法取代方法字符串
一些方法例如setTimeout()/setInterval(),接受字符串或者方法實例作為參數。直接傳遞方法對象作為參數來避免對字 符串的二次解析。
傳遞方法
setTimeout(test, 1);
傳遞方法字符串
setTimeout('test()', 1);
對應的測試可以參考http://jsperf.com/string-function
腳本裝載優化
使用工具精簡腳本
精簡代碼就是將代碼中的空格和注釋去除,也有更進一步的會對變量名稱混淆+精簡。
根據統計精簡后文件大小平均減少21%,即使Gzip之后文件也會減少5%。
常用的工具如下,
例如Closure Compiler效果如下,
啟用Gzip壓縮
Gzip通常可以減少70%網頁內容的大小,包括腳本、樣式表、圖片等文件。Gzip比deflate更高效,主流服務器都有相應的 壓縮支持模塊。
Gzip的工作流程為
- 客戶端在請求Accept-Encoding中聲明可以支持gzip
- 服務器將請求文檔壓縮,並在Content-Encoding中聲明該回復為gzip格式
- 客戶端收到之后按照gzip解壓縮
設置Cache-Control和Expires頭
通過Cache-Control和Expires頭可以將腳本文件緩存在客戶端或者代理服務器上,可以減少腳本下載的時間。
Expires格式: Expires = "Expires" ":" HTTP-date Expires: Thu, 01 Dec 1994 16:00:00 GMT Note: if a response includes a Cache-Control field with the max-age directive that directive overrides the Expires field. Cache-Control格式: Cache-Control = "Cache-Control" ":" 1#cache-directive Cache-Control: public
具體的標准定義可以參考http1.1中的定義,簡單來說Expires控制過期時間是多久,Cache-Control控制什么地方可以緩存 。
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
異步加載腳本
腳本加載與解析會阻塞HTML渲染,可以通過異步加載方式來避免渲染阻塞。
異步加載的方式很多,比較通用的方法是通過類似下面的代碼實現,
function loadjs
(script_filename){
var script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.setAttribute('src', script_filename);
script.setAttribute('id', 'script-id');
scriptElement = document.getElementById('script-id');
if(scriptElement){
document.getElementsByTagName('head')[0].removeChild(scriptElement);
}
document.getElementsByTagName('head')[0].appendChild(script);
}
var script = 'scripts/alert.js';
loadjs(script);
DOM操作優化
DOM操作性能問題主要有以下原因,
- DOM元素過多導致元素定位緩慢
- 大量的DOM接口調用
- DOM操作觸發頻繁的reflow(layout)和repaint
關於reflow(layout)和repaint可以參考下圖,可以看到layout發生在repaint之前,所以layout相對來說會造成更多性能 損耗。
- reflow(layout)就是計算頁面元素的幾何信息
- repaint就是繪制頁面元素
以下是一個wikipedia網站reflow的過程錄像,
減少DOM元素數量
1. 在console中執行命令查看DOM元素數量
document.getElementsByTagName('*').length
2. Yahoo首頁DOM元素數量在1200左右。正常頁面大小一般不應該超過 1000。
3. DOM元素過多會使DOM元素查詢效率,樣式表匹配效率降低,是頁面性能最主要的瓶頸之一。
優化CSS樣式轉換
如果需要動態更改CSS樣式,盡量采用觸發reflow次數較少的方式。
例如以下代碼逐條更改元素的幾何屬性,理論上會觸發多次reflow
element.style.fontWeight = 'bold'; element.style.marginLeft= '30px'; element.style.marginRight = '30px';
可以通過直接設置元素的className直接設置,只會觸發一次reflow
element.className = 'selectedAnchor';
具體的測試結果如下,
測試用例可以參考http://jsperf.com/css-class
優化節點添加
多個節點插入操作,即使在外面設置節點的元素和風格再插入,由於多個節點還是會引發多次reflow。優化的方法是創建 DocumentFragment,在其中插入節點后再添加到頁面。
例如JQuery中所有的添加節點的操作如append,都是最終調用documentFragment來實現的,
http://code.jquery.com/jquery-1.10.2.js
function
createSafeFragment( document ) {
var list = nodeNames.split( "|" ),
safeFrag = document.createDocumentFragment();
if ( safeFrag.createElement ) {
while ( list.length ) {
safeFrag.createElement(
list.pop()
);
}
}
return safeFrag;
}
關於documentFragment對比直接添加節點的測試http://jsperf.com/fragment2
優化節點修改
對於節點的修改,可以考慮使用cloneNode在外部更新節點然后再通過replace與原始節點互換。
var orig = document.getElementById('container');
var clone = orig.cloneNode(true);
var list = ['foo', 'bar', 'baz'];
var contents;
for (var i = 0; i < list.length; i++) {
content = document.createTextNode(list[i]);
clone.appendChild(content);
}
orig.parentNode.replaceChild(clone, orig);
對應的測試可以參考http://jsperf.com/clone-node2
減少使用元素位置操作
一般瀏覽器都會使用增量reflow的方式將需要reflow的操作積累到一定程度然后再一起觸發,但是如果腳本中要獲取以下 屬性,那么積累的reflow將會馬上執行,已得到准確的位置信息。
- offsetLeft
- offsetTop
- offsetHeight
- offsetWidth
- scrollTop/Left/Width/Height
- clientTop/Left/Width/Height
- getComputedStyle()
避免遍歷大量元素
避免對全局DOM元素進行遍歷,如果parent已知可以指定parent在特定范圍查詢。
例如以下示例,
var elements = document.getElementsByTagName('*');
for (i = 0; i < elements.length; i++) {
if (elements[i].hasAttribute('selected')) {}
}
如果已知元素存在於一個較小的范圍內,
var elements = document.getElementById
('canvas').getElementsByTagName('*');
for (i = 0; i < elements.length; i++) {
if (elements[i].hasAttribute('selected')) {}
}
相關測試可以參考http://jsperf.com/ranged-loop
事件優化
使用事件代理
1. 當存在多個元素需要注冊事件時,在每個元素上綁定事件本身就會對性能有一定損耗。
2. 由於DOM Level2事件模 型中所有事件默認會傳播到上層文檔對象,可以借助這個機制在上層元素注冊一個統一事件對不同子元素進行相應處理。
捕獲型事件先發生。兩種事件流會觸發DOM中的所有對象,從document對象開始,也在document對象結束。
http://www.w3.org/TR/2003/NOTE-DOM-Level-3-Events-20031107/events.html
示例代碼如下
<ul id="parent-list"> <li id="post-1">Item 1 <li id="post-2">Item 2 <li id="post-3">Item 3 <li id="post-4">Item 4 <li id="post-5">Item 5 <li id="post-6">Item 6 </li></ul>
// Get the element, add a click listener...
document.getElementById("parent-list").addEventListener("click",function(e) {
// e.target is the clicked element!
// If it was a list item
if(e.target && e.target.nodeName == "LI") {
// List item found! Output the ID!
console.log("List item ",e.target.id.replace("post-")," was clicked!");
}
});
對應的測試可以參考http://jsperf.com/event- delegate
動畫優化
動畫效果在缺少硬件加速支持的情況下反應緩慢,例如手機客戶端
特效應該只在確實能改善用戶體驗時才使用,而不應用於炫耀或者彌補功能與可用性上的缺陷
至少要給用戶一個選擇可以禁用動畫效果
設置動畫元素為absolute或fixed
position: static 或position: relative元素應用動畫效果會造成頻繁的reflow
position: absolute或position: fixed 的元素應用動畫效果只需要repaint
關於position的具體介紹可以參考
http://css- tricks.com/almanac/properties/p/position/
使用一個timer完成多個元素動畫
setInterval和setTimeout是兩個常用的實現動畫的接口,用以間隔更新元素的風格與布局。
動畫效果的幀率最優化的情況是使用一個timer完成多個對象的動畫效果,其原因在於多個timer的調用本身就會損耗一定 性能。
setInterval(function() {
animateFirst('');
}, 10);
setInterval(function() {
animateSecond('');
}, 10);
使用同一個timer,
setInterval(function() {
animateFirst('');
animateSecond('');
}, 10);
以上是JavaScript性能提高的技巧總結,基本上都能夠通過測試驗證,但是限於篇幅沒有把所有的測試結果都 貼出來。
最后再引用一句名人名言作為結尾,
Premature optimization is the root of all evil. -- Donald Knuth








