轉自http://www.ibm.com/developerworks/cn/opensource/os-cn-dojodoh/
有了定位效能瓶頸的工具之後,開發也要具備辨認常見效能瓶頸的能力,以下是 Javascript 中常見的會導致效能下降的問題,以及對這些問題的最佳實務。
函數中使用過多的外部變數
範圍是理解 Javascript 運行機制的關鍵。Javascript 中的函數與我們熟知的 Java 或是 C++ 語言不同,Javascript 函數內部除了可以獲得傳入的參數以外,還可以訪問由 Javascript 引擎存取的 scope 屬性。簡單來說,scope 就是函數的範圍鏈,包含了所有該函數可以訪問的外部變數。
清單 3. 外部變數
function render(){
var body = document.body,
links = document.getElementsByTagName("a");
document.getElementById("button").onclick = function(){
handleClick();
}
…
}
代碼中出現了三次對 document 的引用,由於 document 是存在於 scope 鏈頂端的全域對象,所以每次都會遍曆整個 scope 鏈,對效能影響很大。
最佳實務:
使用局部變數代替外部變數,以減少 javascript 解譯器查詢 scope 鏈次數。清單 4 中,用局部變數 doc 代替全域變數 document,減少了遍曆 scope 的次數。
清單 4. 局部變數 doc 代替全域變數 document
function render(){
var doc = document,
body = doc.body,
links = doc.getElementsByTagName("a");
doc.getElementById("button").onclick = function(){
handleClick();
}
...
}
頻繁出現“.”操作符
點操作符會引起對象的逐層遍曆,對象成員嵌套越深,訪問該對象的速度就越慢。比如 A 對象下有包含 B 對象的 10 個成員,而 B 對象下有包含 C 對象的 20 個成員,那麼在最差的情況下,需要查詢 200 次才能訪問 A.B.C,這種查詢的效能消耗也是很可觀的。
最佳實務:
使用局部變數代替多級對象,以減少“.”操作次數,即降低物件查詢深度
直接操作 NodeList 對象
NodeList 是包含的 DOM 節點引用的類數組對象,包含 length 屬性。NodeList 對象一直處於即時更新的狀態,每當訪問對象屬性(如 length)的時候,都會觸發對應 HTML 物件查詢操作。下面是一個經典的“Live“ NodeList 的例子:
清單 5. 經典的“Live“ NodeList 的例子
var links = document.getElementsByTagName("a");
for(var i = 0; i < links.length;i++){
var link = document.createElement("a")
document.body.appendChild(link);
}
以上代碼原意是將頁面上連結數翻倍,但是每當頁面添加了一個 <a> 節點時,links.length 就自增 1,於是上溯代碼成為死迴圈。
最佳實務:
將 NodeList 轉化為數組後再進行操作。切斷節點引用與底層節點的即時聯絡,從而大大提高遍曆效能。
全域尋找 DOM 節點
由於 HTML 本身查詢節點操作存在效能限制,所以應該儘可能的縮小節點查詢範圍的廣度和深度。
最佳實務:
指定尋找的根節點,縮小遍曆 DOM 樹的大小。
直接對 DOM 樹進行修改
對 DOM 樹的每次可見的修改都會引起 DOM 樹的重排,而重排是代價非常昂貴的操作。會導致介面的拖慢,應盡量減少這類操作。
最佳實務:
使用 DocumentFragment,進行 DOM 的修改。在除去基於 WebKit(Chrome 和 Safari)以外的瀏覽器上,innerHTML 比標準的 DOM 方法(如 createElement 等)更快些;而在以 WebKit 為核心的瀏覽器上,兩者相差無幾。所以最佳的局部頁面修改方案應該是:合成代表 HTML 片段的字串,然後將其賦給對應的 innerHTML。
頻繁修改布局相關樣式
修改布局相關的樣式也會導致頁面的重排,如下面的代碼造成了 3 次頁面重排:
清單 6. 造成頁面的重排
vary element = document.getElementById("div1");
element.style.borderLeft = '1px';
element.style.borderRight = '2px';
element.style.padding = '5px';
最佳實務:
合并修改布局的樣式,減少瀏覽器 reflow 次數。
清單 7. 合并修改布局的樣式
var element = dojo.byId("div1");
element.style.cssText += "; border-left:1px; border-right: 2px; padding: 5px;";
Javascript 代碼已耗用時間超出瀏覽器最大限制
由於大多數瀏覽器使用單個線程執行 Javascript 和更新使用者介面,這意味著 Javascript 處於運行狀態時,使用者介面處於“鎖定”狀態。所以瀏覽器限制了 Javascript 的最長連續已耗用時間,確保瀏覽器不會被惡意代碼或錯誤迴圈伺服器用戶端檔案鎖。當到達瀏覽器限制時,使用者可以看到下面的對話方塊。
圖 8. 指令碼逾時警告
圖 8. 指令碼逾時警告
最佳實務
對於一些需要長時間啟動並執行 Javascript 任務,使用定時器隔斷該任務,使 UI 線程有機會更新介面。
清單 8. 更新介面
setTimeout(function(){
// deferred javascript 1
setTimeout(function(){
// deferred javascript2
setTimeout(function(){
// deferred javascript2
}, 100)
}, 100)
}, 100)
清單 8 中的定時器代碼簡明易懂,但維護性較差,下面是一個更易使用的遞迴版本定時器:
清單 9. 定時器代碼
function timer(argArray, process, context){
setTimeout(function(){
var item = argArray.shift();
process.call(context, item);
if(array.length > 0){
setTimeout(arguments.callee, 100);
}
}, 100)
}