JavaScript 是比較完善的前端開發語言,在現今的 web 開發中應用非常廣泛,尤其是對 Web 2.0 的應用。隨著 Web 2.0 越來越流行的今天,我們會發現:在我們的 web 應用專案中,會有大量的 JavaScript 程式碼,而且以後會越來越多。 JavaScript 作為一個解釋執行的語言,以及它的單線程機制,決定了效能問題是 JavaScript 的軟肋,也是 web 軟體工程師在寫 JavaScript 需要高度重視的問題,尤其是針對 Web 2.0 的應用。絕大多數 web 軟體工程師都或多或少的遇到所開發的 Web 2.0 應用的效能欠佳的問題,其主要原因就是 JavaScript 效能不足,瀏覽器負荷過重。但是,解決這種解釋執行並且單執行緒運作語言的效能問題也並非易事。這篇文章會著重介紹一些關於開發中JavaScript 效能調優的技巧和最佳實踐,同樣也會涉及到關於JavaScript 操作DOM 節點的效能調優的一些方法.
簡介
Web 開發中經常會遇到效能的問題,尤其是針對當今的Web2.0 應用。 JavaScript 是當今使用最廣泛的Web 開發語言,Web 應用的效能問題很大一部分都是由程式設計師寫的JavaScript 腳本效能不佳所造成的,裡麵包括了JavaScript 語言本身的效能問題,以及其與DOM 交互時的性能問題。本文主要來探討如何盡可能多的避免這類問題,從而最大限度的提高 Web 應用的效能。
JavaScript 效能調優
JavaScript 語言由於它的單執行緒和解釋執行的兩個特點,決定了它本身有很多地方有效能問題,所以可改進的地方有不少。
eval 的問題:
比較下述程式碼:
清單1. eval 的問題
var reference = {}, props = “p1”;
eval(“reference.” props “=5”)
var reference = {}, props = “p1”;
reference[props] = 5
有「eval」的程式碼比沒有「eval」的程式碼慢上100 倍以上。
主要原因是:JavaScript 程式碼在執行前會進行類似「預先編譯」的操作:首先會建立一個目前執行環境下的活動對象,並將那些以var 申明的變數設為活動對象的屬性,但是此時這些變數的賦值都是undefined,並將那些以function 定義的函數也加入為活動物件的屬性,而且它們的值正是函數的定義。但是,如果你使用了“eval”,則“eval”中的程式碼(實際上為字串)無法預先識別其上下文,無法事先解析和最佳化,即無法進行預編譯的操作。所以,其性能也會大幅降低。
Function 的用法:
比較下述代碼:
清單2. function 的用法 程式碼如下:
var func1 = new Function(“return arguments[0] arguments[1]”);
func11 (10, 20);
var func2 = function(){ return arguments[0] arguments[1] };
func2(10, 20);
這裡類似之前提到的「eval」方法,這裡「func1」的效率會比「func2」的效率差很多,所以建議使用第二種方式。
函數的作用域鏈(scope chain):
JavaScript 程式碼解釋執行,在進入函數內部時,它會預先分析當前的變量,並將這些變數歸入不同的層級(level),一般情況下:
局部變數放入層級1(淺),全域變數放入層級2(深)。如果進入「with」或「try – catch」程式碼區塊,則會增加新的層級,即將「with」或「catch」裡的變數放入最淺層(層 1),並將先前的層級依序加深。
參考如下代碼:
清單3. 函數作用域鏈
代碼如下:
var myObj = … ..
… ..
function process(){
var images = document.getElementsByTagName("img"), wid = document.getElementsByTagName("input"),
combination = [];
for(var i = 0; i combination.push(combine(images[i] , widget[2*i]));
}
myObj.container.property1 = combination[0];
myObj.container.property2 = combination[combination.length-1];
}
這裡我們可以看到,“images”,“widget”,“combination”屬於局部變量,在層 1。 “document”,“myObj”屬於全域變量,在層 2。
變數所在的層越淺,存取(讀取或修改)速度越快,層越深,存取速度越慢。所以這裡對“images”,“widget”,“combination”的訪問速度比“document”,“myObj”要快一些。所以建議盡量使用局部變量,可見如下程式碼:
清單4. 使用局部變數 複製程式碼
複製程式碼
複製程式碼
複製程式碼
程式碼如下:
var myObj = … ..
… ..
function process(){ var doc = document;
var images = doc. getElementsByTagName("img"),
}
我們用局部變數“doc”取代全域變數“document”,這樣可以改善效能,尤其是對於大量使用全域變數的函數裡面。
再看如下代碼:
清單5. 慎用with
複製代碼
代碼如下:
var myObj = … ..
… ..
function process(){
var doc = document; var images = doc.getElementsByName(" img"),
widget = doc.getElementsByTagName("input"),
combination = []; for(var i = 0; i combination. push(combine(images[i], widget[2*i])); } with (myObj.container) { property1 = combination[0]; property2 = combination[combination .length-1];
}
}
加上「with」關鍵字,我們讓程式碼更加簡潔清晰了,但是這樣做效能會受影響。如同先前所說的,當我們進入「with」程式碼區塊時,「combination」便從原來的層 1 變到了層 2,這樣,效率會大打折扣。所以比較一下,還是用原來的程式碼:
清單6.改進。程式碼如下:
var myObj = … ..
… ..
function process(){
var doc = document;
var images = doc.getElementsByName(doc. "img"),
widget = doc.getElementsByTagName("input"),
combination = [];
for(var i = 0; i combination .push(combine(images[i], widget[2*i]));
}
myObj.container.property1 = combination[0]; myObj.container.property2 = combination[combination. length-1];
}
複製程式碼
程式碼如下:
var myObj = … ..
… ..
function process(){
var doc = document;
var images = doc.getElementsByTagName("img"),
widget = doc.getElementsByTagName("input"),
combination = [];
for(var i = 0; i combination.push(combine(images[i], widget[2*i]));
}
var ctn = myObj.container;
ctn.property1 = combination[0];
ctn.property2 = combination[combination.length-1]; }
我們用局部變數來取代「myObj」的第2 層的「container ”對象。如果有大量的這種對物件深層屬性的訪問,可以參考以上方式提高效能。
字串(String)相關
複製程式碼
代碼如下: str = “str1”str2” str = “str1”str2” str = “str1”str2”
這是我們拼接字串常用的方式,但是這種方式會有一些臨時變數的建立和銷毀,影響效能,所以建議使用如下方式拼接:
清單9. 字串陣列方式拼接
var str str_ >str_array.push(“str1”);
str_array.push(“str2”);
str = str_array.join(“”);
這裡我們利用陣列(array )的「join」方法實作字串的拼接,尤其是程式的舊版的Internet Explore(IE6)上運行時,會有非常明顯的效能上的改進。
當然,最新的瀏覽器(如火狐Firefox3 ,IE8 等等)對字串的拼接做了最佳化,我們也可以這樣寫:
清單10. 字串快速拼接
str =“str1”
str =“str”
新的瀏覽器對「 =」做了最佳化,效能略快於陣列的「join」方法。在不久的將來更新版本瀏覽器可能會對「 」也會進行最佳化,所以那時我們可以直接寫入:str = “str1” “str2”。
隱式型別轉換
參考以下程式碼:
清單11. 隱式型別轉換
複製程式碼
程式碼如下:
var str = “12345678”, arr = [];
for(var i = 0; i arr.push( str.charAt(i));
} 這裡我們在每個循環時都會呼叫字串的“ charAt”方法,但由於我們是將常數“12345678”賦值給“str”,所以“str”這裡事實上並不是一個字串對象,當它每次調用“charAt”函數時,都會臨時構造值為“ 12345678”的字串對象,然後呼叫“charAt”方法,最後再釋放這個字串臨時對象。我們可以做一些改進: 清單12. 避免隱式類型轉換
複製代碼
代碼如下:
var str = new Stirng(“12345678”), arr = []; for(var i = 0; i arr.push( str.charAt(i));
}
這樣一來,變數「str」作為一個字串對象,就不會有這種隱式型別轉換的過程了,這樣一來,效率會顯著提高。
字串匹配
複製程式碼
程式碼如下:
for(var i = 0; i if(str_array[i].match (/^s*extras/)){
……………………
複製程式碼
程式碼如下:
var sExpr = /^s*extras/; for(var i = 0; i if(str_array[i].match(sExpr )){ …………………… } }
這樣就不會有臨時物件了。
setTimeout 和setInterval
「setTimeout」和「setInterval」這兩個函數可以接受字串變量,但是會帶來和之前談到的「eval」類似的效能問題,所以建議還是直接傳入函數對象本身。
利用提前退出
參考如下兩段代碼:
清單15. 利用提前退出
複製程式碼
程式碼如下:
// 程式碼1
var name = … .;
var source = … ;
if(source.match(/ …… /)){
……………………………
}
// 程式碼2
var name = … .;
var source = ... ; if(name.indexOf( … ) &&source.match(/ …… /)){ ……………………………
}
代碼2 多了一個對“name.indexOf( … )”的判斷,這使得程式每次走到這段時會先執行“indexOf”的判斷,再執行後面的“match” ,在「indexOf」比「match」更有效率的前提下,這樣做會減少「match」的執行次數,從而一定程度的提高效率。
----------------------------------------------- ---------------------------------
DOM 操作效能調優
JavaScript 的開發離不開DOM 的操作,所以對DOM 操作的效能調優在Web 開發中也是非常重要的。 Repaint 和 Reflow Repaint 也叫 Redraw,它指的是一種不會影響目前 DOM 的結構和佈局的一種重繪動作。如下動作會產生Repaint 動作:
不可見到可見(visibility 樣式屬性)
顏色或圖片變化(background, border-color, color 樣式屬性)
不改變頁面元素大小,形狀和位置,但改變其外觀的變化
Reflow 比起Repaint 來講就是一種更顯著的改變了。它主要發生在 DOM 樹被操作的時候,任何改變 DOM 的結構和佈局都會產生 Reflow。但當一個元素的 Reflow 操作發生時,它的所有父元素和子元素都會放生 Reflow,最後 Reflow 必然會導致 Repaint 的產生。舉例說明,以下動作會產生Repaint 動作:
瀏覽器視窗的變化
DOM 節點的新增刪除操作
複製程式碼
複製程式碼
複製程式碼
複製程式碼
複製程式碼
var pDiv = document.createElement(“div”);
document.body.appendChild(pDiv);----- reflow
var cDiv1 = document.createElement(“ div”);
var cDiv2 = document.createElement(“div”); pDiv.appendChild(cDiv1);----- reflow pDiv.appendChild(cDiv2);----- reflow
複製程式碼
複製程式碼
複製程式碼>
var pDiv = document.createElement(“div”);
var cDiv1 = document.createElement(“div”); var cDiv2 = document.createElement(“div”); var cDiv2 = document.createElement(“div”); 🎜>pDiv.appendChild(cDiv1); pDiv.appendChild(cDiv2); document.body.appendChild(pDiv);----- reflow 這裡便只有一次reflow,所以我們推薦這種DOM 節點操作的方式。 關於上述較少Reflow 操作的解決方案,還有一個可以參考的模式:清單18. 利用display 減少reflow 複製程式碼 程式碼如下: var pDiv = document.getElementById(“parent”); pDiv.style.display = “none”-🎜>pDiv.style.display = “none”-🎜>pDiv.style.display = “none”-🎜>pDiv.style.display = “none”-🎜>pDiv.style.display = “none”-🎜>pDiv.style.display = "none"- ---- reflow pDiv.appendChild(cDiv1); pDiv.appendChild(cDiv2); pDiv.appendChild(cDiv3); pDiv.appendChild(appendChild(cDiv3); pDiv.appendChild(cDiv4); appendChild(cDiv5); pDiv.style.width = “100px”; pDiv.style.height = “100px”; pDiv.style.display = “block”----- reflow
先隱藏pDiv,再顯示,這樣,隱藏和顯示之間的操作便不會產生任何的Reflow,提高了效率
特殊測量屬性和方法
DOM 元素裡面有一些特殊的測量屬性的存取和方法的調用,也會觸發Reflow,比較典型的就是「offsetWidth」屬性和「getComputedStyle」方法。
圖1. 特殊測量屬性和方法
這些測量屬性和方法大致有這些:
offsetLeft
offsetTop
offsetHeight
HeoffsetWidth
scrollTop/Left/Width/Left/Width/Rft
getComputedStyle()
currentStyle(in IE))
這些屬性和方法的存取和調用,都會觸發Reflow 的產生,我們應該盡量減少對這些屬性和方法的訪問和調用,參考如下代碼:
清單19. 特殊測量屬性
var pe = document.getElementById(“pos_element”);
var result = document.getElementById(“result_element”);
var pOffent.getElementById(“result_element”);
var pOffsetWidth = pepe. result.children[0].style.width = pOffsetWidth;
result.children[1].style.width = pOffsetWidth;
result.children[2].style.width = pOffsetWidth;
…………其他修改…………
這裡我們可以用臨時變數將「offsetWidth」的值快取起來,這樣就不用每次存取「offsetWidth」屬性。這種方式在循環裡面非常適用,可以大大提高效能。
樣式相關
我們一定常常看到如下的程式碼:
清單20. 樣式相關
var sElement = document.getElementById(“pos_element”); sElement.style.b =' 1px solid red '
sElement.style.backgroundColor = ' silver '
sElement.style.padding = ' 2px 3px '
sElement.style.marginLeft = ' 5px '
但可以看到,這裡的每一個樣式的改變,都會產生Reflow。需要減少這種情況的發生,我們可以這樣做:
複製程式碼
程式碼如下:
.class1 {
border: ' 1px solid red '
background-color: ' silver '
padding: ' 2px 3px '
margin-left: ' 5px '
} document.getElementById(“pos_element”).className = 'class1' ;
🎜>用class 取代style,可以將原有的所有Reflow 或Repaint 的次數都縮減到一個。
解決方案2
:
清單22. cssText 解決方案
>
程式碼如下: var sElement = document.getElementById(“pos_element”); var newStyle = ' border: 1px solid red; ' background-color; silver; '
' padding: 2px 3px; ' “margin-left: 5px;” sElement.style.cssText = newStyle; 一次設定所有樣式,也是減少Reflow提高性能的方法。 XPath 一個頁面上往往包含 1000 多頁面元素,在定位具體元素的時候,往往需要一定的時間。如果用 id 或 name 定位可能效率不會太慢,如果用元素的一些其他屬性(例如 className 等等)定位,可能效率有不理想了。有的可能只能透過遍歷所有元素(getElementsByTagName)然後過濾才能找到對應元素,這就更加低效了,這裡我們推薦使用 XPath 查找元素,這是許多瀏覽器本身支援的功能。 清單 23. XPath 解決方案 複製程式碼 程式碼
if(document.evaluate){
var tblHeaders = document.evaluate(“//body/div/table//th”);
var result = tblHeaders.iterateNext();
while(result) {
result.style.border = “1px dotted blue”;
result ………………
result = xpathResult.iterateNext();
}
}
} else{ //getElementsByTagName() ……
// 處理瀏覽器不支援XPath 的情況
………………………………
}
瀏覽器XPath 的搜尋引擎會優化搜尋效率,大幅縮短結果回傳時間。
HTMLCollection 物件
這是一類特殊的對象,它們有點像數組,但不完全是數組。下述方法的回傳值一般都是HTMLCollection 物件:
document.images, document.forms
getElementsByTagName()
getElementsByClassName()
這些HTMLCollection 物件並不是固定的值,而是固定的值,而是固定的值動態的結果。它們是一些比較特殊的查詢的回傳值,在下列情況下,它們會重新執行先前的查詢而得到新的回傳值(查詢結果),雖然多數情況下會和前一次或幾次的回傳值都一樣:
Length 屬性
具體的某個成員
所以,HTMLCollection 物件對這些屬性和成員的訪問,比起數組來慢很多。當然也有例外,Opera 和 Safari 對這種情況就處理的很好,不會有太大效能問題。
參考如下代碼: 清單24. HTMLConnection 對象
代碼如下:
var items = [“test1”, “test2”, “test3”, ……………… ];
for(var i = 0; i ………………………………
}
var items = document.getElementsByTagName(“div”);
for(var i = 0; i …………………………………… .
}
上述兩端代碼,下面的效率比起上面一段要慢很多,因為每個循環都會有「items.length」的觸發,也會導致「document.getElementsByTagName(..)」方法的再次調用,這便是效率便會大幅度下降的原因。我們可以這樣解決: 清單25. HTMLConnection 對象解決方案
代碼如下:
var items = document.getElementsByTagName(“div”);
var len = items.length
for(var i = 0; i …………………………………… .
}
這樣一來,效率基本上與普通數組一樣。
動態建立script 標籤
載入並執行一段JavaScript 腳本是需要一定時間的,在我們的程式中,有時候有些JavaScript 腳本被載入後基本上沒有被使用過(例如:腳本裡的函數從來沒有被調用等等)。載入這些腳本只會佔用 CPU 時間和增加記憶體消耗,降低 Web 應用的效能。所以推薦動態的載入 JavaScript 腳本文件,尤其是那些內容較多,消耗資源較大的腳本文件。 清單26. 建立script 標籤
程式碼複製程式碼
程式碼
if(needXHR){
document.write(“
if(dojo.isIE){
document.write(“ --------- -------------------------------------------------- --------------------- 結束語 這篇文章介紹了Web 開發中關於效能方面需要注意的一些小細節,從JavaScript 本身著手,介紹了JavaScript 中需要避免的一些函數的使用和程式設計規則,例如eval 的弊端,function scope chain 以及String 的用法等等,也分享了一些比較推薦的做法,並擴展到JavaScript對DOM 操作的效能調優,例如利用Repaint 和Reflow 的機制,如何使用特殊測量屬性,樣式相關的效能調優以及HTMLCollection 物件的原理和使用小技巧。這些小細節我們可以在開發過程中盡量注意一下,以盡可能多的提升我們 Web 應用的效能。