javascript性能優化

        現在主流瀏覽器都在比拼JavaScript引擎的執行速度,但最終都會達到一個理論極限,即無限接近編譯後程序執行速度。 這種狀況下決定程序速度的另外一個重要因素就是代碼自己。 在這裏咱們會分門別類的介紹JavaScript性能優化的技巧,並提供相應的測試用例,供你們在本身使用的瀏覽器上驗證, 同時會對特定的JavaScript背景知識作必定的介紹。javascript

目錄

變量查找優化

變量聲明帶上var

1. 若是聲明變量忘記了var,那麼js引擎將會遍歷整個做用域查找這個變量,結果無論找到與否,都是悲劇。

  • 若是在上級做用域找到了這個變量,上級做用域變量的內容將被無聲的改寫,致使莫名奇妙的錯誤發生。

  • 若是在上級做用域沒有找到該變量,這個變量將自動被聲明爲全局變量,然而卻都找不到這個全局變量的定義。

2. 基於上面邏輯,性能方面不帶var聲明變量天然要比帶var速度慢

具體能夠參考http://jsperf.com/withvar-withoutvar。下面是個簡單的結果截圖,藍色爲帶var的狀況,越長說明 速度越快。

image

慎用全局變量

1. 全局變量須要搜索更長的做用域鏈。

2. 全局變量的生命週期比局部變量長,不利於內存釋放。

3. 過多的全局變量容易形成混淆,增大產生bug的可能性。

全局變量與局部變量的測試能夠參考http://jsperf.com/local-global-var

以上兩條還能夠得出一條JavaScript經常使用的編程風格具備相同做用域變量經過一個var聲明 。

這樣方便查看該做用域全部的變量,JQuery源代碼中就是用了這種風格。例以下面源代碼

https://github.com/jquery/jquery/blob/master/src/core.js

1
2
3
4
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

1
2
3
4
5
6
7
8
9
10
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語句將一個新的可變對象推入做用域鏈的頭部,函數的全部局部變量如今處於第二個做用域鏈對象中,從而使局部變 量的訪問代價提升。

1
2
3
4
5
6
7
8
9
10
11
var person = {
    name: 「Nicholas",
    age: 30
}
function displayInfo() {
    var count = 5;
    with (person) {
        alert(name + ' is ' + age);
        alert( 'count is ' + count);
    }
}

以上代碼的結果將name和age兩個變量推入第一個做用域,以下圖所示,

image

使用with與不使用with的測試能夠參考http://jsperf.com/with-with

核心語法優化

經過原型優化方法定義

1. 若是一個方法類型將被頻繁構造,經過方法原型從外面定義附加方法,從而避免方法的重複定義。
2. 能夠經過外 部原型的構造方式初始化值類型的變量定義。(這裏強調值類型的緣由是,引用類型若是在原型中定義, 一個實例對引用類型的更改會影響到其餘實例。)

這條規則中涉及到JavaScript中原型的概念,

  • 構造函數都有一個prototype屬性,指向另外一個對象。這個對象的全部屬性和方法,都會被構造函數的實例繼承。咱們可 以把那些不變的屬性和方法,直接定義在prototype對象上。

  • 能夠經過對象實例訪問保存在原型中的值,不能經過對象實例重寫原型中的值。

  • 在實例中添加一個與實例原型同名屬性,那該屬性就會屏蔽原型中的屬性。

  • 經過delete操做符能夠刪除實例中的屬性。

例如如下代碼以及相應的內存中原型表示以下,

1
2
3
4
5
6
7
8
9
10
11
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」

image

原型附加方法測試能夠參考http://jsperf.com/func-constructor

原型附加值類型變量測試能夠參考http://jsperf.com/prototype2

避開閉包陷阱

1. 閉包是個強大的工具,但同時也是性能問題的主要誘因之一。不合理的使用閉包會致使內存泄漏。

2. 閉包的性能不如使用內部方法,更不如重用外部方法。

因爲IE瀏覽器的DOM是用COM來實現的, COM的內存管理是經過引用計數的方式,引用計數有個難題就是循環引用,一旦DOM 引用了閉包(例如event handler),閉包的上層元素又引用了這個DOM,就會形成循環引用從而致使內存泄漏。

Figure 2 Circular References with Closures

關於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. 添加屬性訪問方法只是增長了一層重定向 ,對於訪問控制沒有意義。

使用屬性訪問方法示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);

直接訪問屬性示例

1
2
3
4
5
6
7
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

1
2
3
for ( var i = 0; i < 200; i++) {
try {} catch (e) {}
}

循環外使用try-catch

1
2
3
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...

1
2
3
4
5
6
<code> 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.
}</code>

can sometimes be totally different from the other...

1
2
3
4
5
<code> var a = [];
a[5] = 5;
for ( var x in a) {
    // Shows only the explicitly set index of "5", and ignores 0-4
}</code>

Also consider that JavaScript libraries might do things like this, which will affect any array you create:

1
2
3
4
5
6
7
8
9
10
11
<code> // 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'.
}</code>

關於for和for…in…的測試能夠看http://jsperf.com/forin

使用原始操做代替方法調用

方法調用通常封裝了原始操做,在性能要求高的邏輯中,可使用原始操做代替方法調用來提升性能。

原始操做

1
var min = a < b ? a : b;

方法實例

1
var min = Math.min(a, b);

關於方法調用和原始操做的測試參考http://jsperf.com/operator-function

傳遞方法取代方法字符串

一些方法例如setTimeout()/setInterval(),接受字符串或者方法實例做爲參數。直接傳遞方法對象做爲參數來避免對字 符串的二次解析。

傳遞方法

1
setTimeout(test, 1);

傳遞方法字符串

1
setTimeout( 'test()' , 1);

對應的測試能夠參考http://jsperf.com/string-function

腳本裝載優化

使用工具精簡腳本

精簡代碼就是將代碼中的空格和註釋去除,也有更進一步的會對變量名稱混淆+精簡。

根據統計精簡後文件大小會平均減小21%,即便Gzip以後文件也會減小5%。

經常使用的工具以下,

例如Closure Compiler效果以下,

image

啓用Gzip壓縮

Gzip一般能夠減小70%網頁內容的大小,包括腳本、樣式表、圖片等文件。Gzip比deflate更高效,主流服務器都有相應的 壓縮支持模塊。

Gzip的工做流程爲

  • 客戶端在請求Accept-Encoding中聲明能夠支持gzip

  • 服務器將請求文檔壓縮,並在Content-Encoding中聲明該回復爲gzip格式

  • 客戶端收到以後按照gzip解壓縮

image

設置Cache-Control和Expires頭

經過Cache-Control和Expires頭能夠將腳本文件緩存在客戶端或者代理服務器上,能夠減小腳本下載的時間。

1
2
3
4
5
6
7
8
9
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渲染,能夠經過異步加載方式來避免渲染阻塞。

異步加載的方式不少,比較通用的方法是經過相似下面的代碼實現,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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就是繪製頁面元素

image

如下是一個wikipedia網站reflow的過程錄像,

減小DOM元素數量

1. 在console中執行命令查看DOM元素數量

1
    document.getElementsByTagName( '*' ).length

2. Yahoo首頁DOM元素數量在1200左右。正常頁面大小通常不該該超過 1000。
3. DOM元素過多會使DOM元素查詢效率,樣式表匹配效率下降,是頁面性能最主要的瓶頸之一。

優化CSS樣式轉換

若是須要動態更改CSS樣式,儘可能採用觸發reflow次數較少的方式。

例如如下代碼逐條更改元素的幾何屬性,理論上會觸發屢次reflow

1
2
3
element.style.fontWeight = 'bold' ;
element.style.marginLeft= '30px' ;
element.style.marginRight = '30px' ;

能夠經過直接設置元素的className直接設置,只會觸發一次reflow

1
2
3
element.className =
'selectedAnchor' ;

具體的測試結果以下,

image

測試用例能夠參考http://jsperf.com/css-class

優化節點添加

多個節點插入操做,即便在外面設置節點的元素和風格再插入,因爲多個節點仍是會引起屢次reflow。優化的方法是建立 DocumentFragment,在其中插入節點後再添加到頁面。

例如JQuery中全部的添加節點的操做如append,都是最終調用documentFragment來實現的,

http://code.jquery.com/jquery-1.10.2.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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與原始節點互換。

1
2
3
4
5
6
7
8
9
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()

具體討論能夠參考這個連接http://www.stubbornella.org/content/2009/03/27/reflows-repaints-css- performance-making-your-javascript-slow/#comment-13157

避免遍歷大量元素

避免對全局DOM元素進行遍歷,若是parent已知能夠指定parent在特定範圍查詢。

例如如下示例,

1
2
3
4
var elements = document.getElementsByTagName( '*' );
for (i = 0; i < elements.length; i++) {
  if (elements[i].hasAttribute( 'selected' )) {}
}

若是已知元素存在於一個較小的範圍內,

1
2
3
4
5
6
var elements = document.getElementById
( 'canvas' ).getElementsByTagName( '*' );
for (i = 0; i < elements.length; i++) {
  if (elements[i].hasAttribute( 'selected' )) {}
}
1

相關測試能夠參考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

image

示例代碼以下

<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);
相關文章
相關標籤/搜索