用一周的時間看完了《高性能Javascript》,涉及到了javascript很多個方面的性能問題,但由于書比較薄,基本點到即止,但還是刷新了我之前的一些對javascript的誤解甚至誤用,還讓我了解到了javscript的一些冷知識和獨到的技巧。讀書過程對其中一部分做了筆記,但沒有全部,有些沒有在電腦前看就沒有記錄下來了。
JS 從循環(huán)性能到 Duff's Device
JS的循環(huán)種類和性能:
1、標準for循環(huán)
for (var i = 0, len = list.length; i < len; i++) {
// todo...
}
2、while循環(huán)
又分前側循環(huán)和后測循環(huán):
while(i--) {
// todo ...
}
do {
// todo ...
} while(i--)
3、for-in 循環(huán)
for (var i in obj) {
// todo
}
前兩種循環(huán)的性能基本一樣,唯獨第三種循環(huán)的性能明顯比前兩種低,問題在于每次迭代操作都會搜索實例或者實例屬性, 速度大約為其他兩種的1/7,除非你需要迭代屬性未知的對象,否則盡量不要使用這種方法去迭代。
寧可羅列出該對象的屬性,然后用for循環(huán)去遍歷該對象:
var props = ['props1', 'props2']
, i
;
for (i = 0; i < props.length; i++) {
process(obj[props[i]]);
}
減少迭代中的操作次數(shù)
每次迭代,減少對遍歷對象的屬性讀取如長度,對于重復的存為局部變量,減少初始化的變量,這些應該很好理解:
var i = arr.length;
for (; i > 0;i--) {
// todo
|
** 再深一步:減少迭代的次數(shù) **
Duff's Device 是一種循環(huán)體展開技術,它讓每一次迭代執(zhí)行了多次迭代的操作,從而減少了迭代的次數(shù)。
var iterations = Math.floor(items.length / 8)
, start_at = item.length % 8
, i = 0
;
do {
switch(start_at ) {
case 0: process(items[i++]);
case 7: process(items[i++]);
case 6: process(items[i++]);
case 5: process(items[i++]);
case 4: process(items[i++]);
case 3: process(items[i++]);
case 2: process(items[i++]);
case 1: process(items[i++]);
}
start_at = 0;
} while(iterations--);
如果迭代次數(shù)超過1000,執(zhí)行效率會提高70%左右,筆者試驗了結果,發(fā)現(xiàn)無論是否是使用了這種技術,效率上并沒有所謂的提升,Demo
** 優(yōu)化這種技術 **
var leftover = items.length % 8
, i = 0
;
while(leftover--) {
process(items[i++]);
}
var iterations = Math.floor(items.length / 8);
while(iterations--) {
process(items[i++]);
}
** ECMA第四版提供了一個新的原生數(shù)組方法forEach(), **
盡管該方法能很便利地去遍歷每個成員,但是它仍然比基于循環(huán)的迭代要慢一些,對每一數(shù)組項調用外部方法所帶來的開銷是速度慢的主要原因
遞歸與調用棧
** 調用棧的限制引出安全問題 **
Javascript引擎支持的遞歸數(shù)量與Javascript調用棧大小直接相關, 只有IE例外,它的調用棧和系統(tǒng)空閑內存有關,其他瀏覽器都有固定數(shù)量的調用棧閑置。
遞歸可能是自身調用的遞歸,可能是相互的遞歸,如A調用B, B調用A,這種情況如果出錯很難找到問題,為了安全,盡可能地使用迭代的方式。
** 迭代來解決安全問題 **
使用優(yōu)化后的迭代替代長時間運行的遞歸函數(shù)可以提升性能,因為運行一個循環(huán)比反復調用一個函數(shù)的開銷要少得多。
** 使用內存緩存來減少遞歸的調用 **
function menfactorial(n) {
if (!menfactorial.cache) {
menfactorial.cache = {
'0': 1,
'1': 1
}
}
if (!menfactorial.cache.hasOwnProperty(n)) {
menfactorial.cache[n] = n * menfactorial(n-1)
}
return menfactorial.cache[n];
}
字符串和正則表達式
** 使用數(shù)組連接字符串更快?**
這句話對又不對,之所以會出現(xiàn)使用數(shù)組連接字符串,是由于傻逼的IE7的連接算法要求瀏覽器在循環(huán)過程中為逐漸增大的字符串他不斷復制并分配內存,結果是運行時間和內存消耗以平方關系遞增,于是當我們使用數(shù)組去連接時,由于一次分配夠了內存,性能就得到了很大的提升。
然而對于IE8后和其他瀏覽器,數(shù)組連接和普通的字符串連接性能差異其實不大,詳情可以看DEMO
** concat **
String.prototype.concat 又怎么樣呢?比使用簡單的+和+=稍慢,比join也慢,所以不建議使用。
** 慎用正則表達式 **
從編譯、設置起始位置、匹配每個正則表達式到匹配成功與失敗
正則中的回溯是影響其速度的重要部分
trim的實現(xiàn):
if (!String.prototype.trim) {
String.prototype.trim = function() {
return this.replace(/^\s+/, '').replace(/\s+$/, '');
}
}
快速響應的用戶界面
** 100毫秒準則 **
如果界面在超過100毫秒響應用戶的輸入,那么用戶會感覺自己與界面失去了聯(lián)系,由于js運行時無法更新UI,所以如果javascript的運行超過100毫秒,用戶會感覺失去了對界面的控制
除了腳本運行的時間,如果網(wǎng)站采用了異步腳本加載的技術,那么還要考慮腳本加載過來的時間,這個時候采用預加載的方式可以減少由于腳本加載而帶來的延遲,比如但用戶點擊某個按鈕觸發(fā)腳本加載時,我們可以改為但用戶把鼠標放在按鈕上時就去加載腳本,這樣,等到用戶去點擊的時候,腳本就已經(jīng)加載好了。
** 定時器 **
當腳本運行時間超過100毫秒,需要想辦法分割執(zhí)行代碼段,從而讓界面能夠響應用戶的交互:
function processArray(items, process, callback) {
var todo = items.concat();
setTimeout(function() {
process(todo.shift());
if (todo..length > 0) {
setTimeout(arguments.callee, 25);8
} else {
callback(items);
}
}, 25);
}
使用定時器對循環(huán)進行分割是一種方法,請看demo,同理,你也可以對要執(zhí)行的函數(shù)隊列進行分割執(zhí)行,同時記錄每個函數(shù)的運行時間。
注意一個頁面最好只有一個定時器,一方面我們可以更好地控制時間隊列,防止多個定時器搶占時間隊列,另一方面防止過多的時間器導致時間分割過小反而引起性能的問題。
** Web Worker **
新版瀏覽器支持的特性,允許在UI線程外執(zhí)行javascript代碼,從而避免鎖定UI
Ajax
五中常用技術用于向服務器請求數(shù)據(jù)
- XML
- Dynamic script tag insertion
- iframes
- Comet
- Multipart XHR
編程過程的性能消耗
** 昂貴的雙重求值 **
js和其他腳本語言一樣,允許我們在程序中提取一個包含代碼的字符串,然后動態(tài)執(zhí)行它,可以通過eval、function構造函數(shù)和兩個定時器
var num1 = 1, num2 = 2
;
var sum = eval('num1 + num2');
var sum2 = new Function('arg1', 'arg2', 'return arg1 + arg2');
setTimeout('sum = num1 + sum2', 1000);
setInterval('sum = num1 + sum2', 1000);
字符串代碼會以正常方式求值,然后再對里面的代碼進行求值,代價昂貴。
** 直接使用Object/ Array直接量 **
盡管我們可以使用像其他語言的方法一樣先實例化構造函數(shù),然后再賦值:
var obj = new Object();
obj.name = 'cjs';
obj.count = 30;
當使用直接量更加快,不僅代碼量小了,運行更加快
var obj = {
name: 'cjs',
count: 30
}
** 使用延遲加載和預加載 **
前端經(jīng)常要兼容各個瀏覽器,導致了對于一個功能的函數(shù),往往需要判斷用戶所在的瀏覽器再執(zhí)行不同的功能,我們可以使用延遲加載或者預加載的方式來防止每次執(zhí)行時的判斷
** 位運算 **
基于底層操作的位運算是javascript運行最快的部分之一,使用的好可以提升一倍以上的速度,這是為什么很多圖形處理的代碼都使用位運算符進行計算的原因之一。
使用與運算符 & 可以用來替代判斷奇數(shù)和偶數(shù),使用或運算符可以用來組合數(shù)字,還有按位左移(<< )和按位右移(>>)和無符號右移(<<)等運算符
** 原生方法 **
javascript的原生部分都是用低級語言寫的,如C++, 意味著這些方法已經(jīng)被編譯為機器碼,成為瀏覽器的一部分,所以永遠比我們自己寫的代碼要快。
如Math對象提供的方法、css選擇器API等,使用原生的querySelector查詢所用的時間是jq查詢的10%
### 構建部署高性能的javascript應用
前端構建工具提供了代碼文件的合并、壓縮,HTML5的離線應用緩存等概念