Javascript 性能優(yōu)化

Javascript最初是解釋型語言渤滞,現(xiàn)在,主流瀏覽器內(nèi)置的Javascript引擎基本上都實(shí)現(xiàn)了Javascript的編譯執(zhí)行,即使如此揉稚,我們?nèi)孕枰獌?yōu)化自己寫的Javascript代碼,以獲得最佳性能熬粗。

注意作用域

避免全局作用域

在之前的文章Javascript 變量搀玖、作用域和內(nèi)存問題提到過,由于訪問變量需要在作用域鏈上進(jìn)行查找驻呐,相比于局部變量灌诅,訪問全局變量的開銷更大芳来,因此以下代碼:

var person = {
    name: "Sue",
    hobbies: ["Yoga", "Jogging"]
};
function hobby() {
    for(let i=0; i<person.hobbies.length; i++) {
        console.log(person.hobbies[i]);
    }
}

可以進(jìn)行如下優(yōu)化:

function hobby() {
    let hobbies = person.hobbies;
    for(let i=0; i<hobbies.length; i++) {
        console.log(hobbies[i]);
    }
}

把需要頻繁訪問的全局變量賦值到局部變量中,可以減小查找深度猜拾,進(jìn)而優(yōu)化性能即舌。
當(dāng)然,上述優(yōu)化過的代碼仍然有不足的地方挎袜,后面的部分會提到顽聂。

避免使用with

為什么避免使用with?

  1. with并不是必須的,使用局部變量可以達(dá)到同樣的目的
  2. with創(chuàng)建了自己的作用域盯仪,相當(dāng)于增加了作用域內(nèi)部查找變量的深度
    舉一個例子:
function test() {
    var innerW = "";
    var outerW = "";
    with(window) {
        innerW = innerWidth;
        outerW = outerWidth;
    }
    return "Inner W: " + innerW + ", Outer W: " + outerW;
}
test()
// "Inner W: 780, Outer W: 795"

上述代碼中紊搪,with作用域減小了對全局變量window的查找深度,不過與此同時(shí)磨总,也增加了作用域中局部變量innerWouterW的查找深度嗦明,功過相抵。
因此我們不如使用局部變量替代with

function test() {
    var w = window;
    var innerW = w.innerWidth;
    var outerW = w.outerWidth;
    return "Inner W: " + innerW + ", Outer W: " + outerW;
}

上述代碼仍然不是最優(yōu)的蚪燕。

算法復(fù)雜度

一下表格列出了幾種算法復(fù)雜度:

復(fù)雜度 名稱 描述
O(1) 常數(shù) 無論多少值娶牌,執(zhí)行時(shí)間恒定,比如使用簡單值或訪問存貯在變量中的值
O(lg n) 對數(shù) 總執(zhí)行時(shí)間與值的數(shù)量相關(guān)馆纳,但不一定需要遍歷每一個值
O(n) 線性 總執(zhí)行時(shí)間與值的數(shù)量線性相關(guān)
O(n2) 平方 總執(zhí)行時(shí)間與值的數(shù)量相關(guān)诗良,每個值要獲取n次

O(1)

如果我們直接使用字面量,或者訪問保存在變量中的值鲁驶,時(shí)間復(fù)雜度為O(1)鉴裹,比如:

var value = 5;
var sum = 10 + value;

上述代碼進(jìn)行了三次常量查找,分別是5钥弯,10径荔,value,這段代碼整體復(fù)雜度為O(1)
訪問數(shù)組也是時(shí)間復(fù)雜度為O(1)的操作脆霎,以下代碼整體復(fù)雜度為O(1):

var values = [1, 2];
var sum = values[0] + values[1];

避免不必要的屬性查找

在對象上訪問屬性是一個O(n)的操作总处,Javascript 面向?qū)ο蟮某绦蛟O(shè)計(jì)(原型鏈與繼承)文中提到過,訪問對象中的屬性時(shí)睛蛛,需要沿著原型鏈追溯查找鹦马,屬性查找越多,執(zhí)行時(shí)間越長忆肾,比如:

var persons = ["Sue", "Jane", "Ben"];
for(let i=0; i<persons.length; i++) {
    console.log(persons[i]);
}

上述代碼中荸频,每次循環(huán)都會比較i<persons.length,為了避免頻繁的屬性查找客冈,可以進(jìn)行如下優(yōu)化:

var persons = ["Sue", "Jane", "Ben"];
for(let i=0, len = persons.length; i<len ; i++) {
    console.log(persons[i]);
}

即如果循環(huán)長度在循環(huán)開始時(shí)即可確定旭从,就將要循環(huán)的長度在初始化的時(shí)候聲明為一個局部變量。

優(yōu)化循環(huán)

由于循環(huán)時(shí)反復(fù)執(zhí)行的代碼,動輒上百次遇绞,因此優(yōu)化循環(huán)時(shí)性能優(yōu)化中很重要的部分键袱。

減值迭代

為什么要進(jìn)行減值迭代,我們比較如下兩個循環(huán):

var nums = [1, 2, 3, 4];
for(let i=0; i<nums.length; i++) {
    console.log(nums[i]);
}
for(let i=nums.length-1; i>-1; i--) {
    console.log(nums[i]);
}

二者有如下區(qū)別:

  1. 迭代順序不同
  2. 前者支持動態(tài)增減數(shù)組元素摹闽,后者不支持
  3. 后者性能優(yōu)于前者蹄咖,前者每次循環(huán)都會計(jì)算nums.length,頻繁的屬性查找降低性能
    因此付鹿,出于性能的考慮澜汤,如果不在乎順序,迭代長度初始即可確定舵匾,使用減值迭代更優(yōu)俊抵。
簡化終止條件

上述情況,我們也可以不使用減值迭代坐梯,即像上文提到過的徽诲,在初始化時(shí)即將迭代長度賦值給一個局部變量。

簡化循環(huán)體

循環(huán)體應(yīng)最大程度地被優(yōu)化吵血,避免進(jìn)行不必要的密集的計(jì)算

使用while循環(huán)

為什么使用while循環(huán)谎替,我們可以比較如下兩個循環(huán):

var len = nums.length;
for(let i=0; i<len; i++) {
    console.log(nums[i]);
}
var i = nums.length ;
while(--len > -1) {
    console.log(nums[len]);
}

以上兩個循環(huán)有一個很明顯的不同點(diǎn):while循環(huán)將每次循環(huán)終止條件的判斷和index的自增合并為一個語句,在后續(xù)部分會講解語句數(shù)量與性能優(yōu)化的關(guān)系蹋辅。

展開循環(huán)

由于建立循環(huán)和處理終止條件需要額外的開銷钱贯,因此如果循環(huán)次數(shù)比較少,而且可以確定侦另,我們可以將其展開秩命,比如:

process(nums[0]);
process(nums[1]);

如果迭代次數(shù)不能事先確定,可以使用Duff裝置褒傅,其中比較著名的是Andrew B. King提出的一種Duff技術(shù)弃锐,通過計(jì)算迭代次數(shù)是否為8的倍數(shù)將循環(huán)展開,將“零頭”與“整數(shù)”分成兩個單獨(dú)的do-while循環(huán)殿托,在處理大數(shù)據(jù)集時(shí)優(yōu)化效果顯著:

var iterations = Math.floor(values.length / 8);
var leftover = values.length % 8;
var i = 0;
if (leftover > 0){
do {
process(values[i++]);
} while (--leftover > 0);
}
do {
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
} while (--iterations > 0);

避免雙重解釋

eval() Function() setTimeout()可以傳入字符串拿愧,Javascript引擎會將其解析成可以執(zhí)行的代碼,意味著碌尔,Javascript執(zhí)行到這里需要額外開一個解釋器來解析字符串,會明顯降低性能券敌,因此:

  1. 盡量避免使用eval()
  2. 避免使用Function構(gòu)造函數(shù)唾戚,用一般function來代替
  3. setTimeout()傳入函數(shù)作為參數(shù)

其他

使用原生方法

原生方法都是用C/C++之類的編譯語言寫出來的,比Javascript快得多待诅。

使用switch語句

多個if-else可以轉(zhuǎn)換為switch語句叹坦,還可以按照最可能到最不可能排序case

使用位運(yùn)算符

當(dāng)進(jìn)行數(shù)學(xué)運(yùn)算的時(shí)候,位運(yùn)算操作要比任何布爾運(yùn)算或者算數(shù)運(yùn)算快卑雁。選擇性地用位運(yùn)算替換算數(shù)運(yùn)算可以極大提升復(fù)雜計(jì)算的性能募书。諸如取模绪囱,邏輯與和邏輯或都可
以考慮用位運(yùn)算來替換。

書中的這段話筆者表示不能理解莹捡,由于使用&& ||做邏輯判斷時(shí)鬼吵,有的時(shí)候只需要求得第一個表達(dá)式的結(jié)果便可以結(jié)束運(yùn)算,而& |無論如何都要求得兩個表達(dá)式的結(jié)果才可以結(jié)束運(yùn)算篮赢,因此后者的性能沒有占太大優(yōu)勢齿椅。
這里,補(bǔ)充一下位運(yùn)算符如何發(fā)揮邏輯運(yùn)算符的功能启泣,首先看幾個例子:

7 === 7 & 6 === 6
1
7 === 7 & 5 === 4
0
7 === 7 | 6 ===6
1
7 === 7 | 7 ===6
1
7 === 6 | 6 === 5
0

也許你會恍然大悟涣脚,位運(yùn)算符并沒有產(chǎn)生truefalse,它只是利用了Number(true) === 1 Number(false) === 0 Boolean(1) === true Boolean(0) === false寥茫。

最小化語句數(shù)

Javascript代碼中的語句數(shù)量會影響執(zhí)行的速度遣蚀,盡量組合語句,可以減少腳本的執(zhí)行時(shí)間纱耻。

多個變量聲明

當(dāng)我們需要聲明多個變量芭梯,比如:

var name = "";
var age = 18;
var hobbies = [];

可以做如下優(yōu)化:

var name = "",
    age = 18,
    hobbies = [];

合并迭代值

上文中我們提到一個例子,使用while循環(huán)可以合并自減和判斷終止條件膝迎,我們還可以換一種寫法:

var i = nums.length ;
while(len > -1) {
    console.log(nums[len--]);
}

即將自減與使用index取值合并為一個語句粥帚。

使用字面量創(chuàng)建數(shù)組和對象

即將如下代碼:

var array = new Array();
array[0] = 1;
array[1] = 2;

var person = new Object();
person.name = "Sue";
person.age = 18;

替換成:

var array = [1, 2];
var person = { name:"Sue", age:18 };

省了4行代碼。

優(yōu)化DOM操作

DOM操作是最拖累性能的一方面限次,優(yōu)化DOM操作可以顯著提高性能芒涡。

最小化現(xiàn)場更新的次數(shù)

如果我們要修改的DOM已經(jīng)顯示在頁面,那么我們就是在做現(xiàn)場更新卖漫,由于每次更新瀏覽器都要重新計(jì)算费尽,重新渲染,非常消耗性能羊始,因此我們應(yīng)該最小化現(xiàn)場更新的次數(shù)旱幼,比如我們要向頁面添加一個列表:

var body = document.getElementsByTagName("body")[0];
for(let i=0; i<10; i++) {
    item = document.createElement("span");
    body.appendChild(item);
    item.appendChild(document.createTextNode("Item" + i));
}

每次循環(huán)時(shí)都會進(jìn)行兩次現(xiàn)場更新,添加div突委,為div添加文字柏卤,總共需要20次現(xiàn)場更新,頁面要重繪20次匀油。
現(xiàn)場更新的性能瓶頸不在于更新的大小缘缚,而在于更新的次數(shù),因此敌蚜,我們可以將所有的更新一次繪制到頁面上桥滨,有以下兩個方法:

文檔片段

可以使用文檔片段先收集好要添加的元素,最后在父節(jié)點(diǎn)上調(diào)用appendChild()將片段的子節(jié)點(diǎn)添加到父節(jié)點(diǎn)中,注意齐媒,片段本身不會被添加蒲每。

<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <div id="container" style="with: 100px; height: 100px; border: 1px solid black;">
            <div id="child">this</div>
        </div>
        <script>
            var container = document.getElementById("container"),
                fragment = document.createDocumentFragment(),
                item,
                i;
            for (i=0; i < 10; i++) {
              item = document.createElement("li");
              fragment.appendChild(item);
              item.appendChild(document.createTextNode("Item " + i));
            }
            container.appendChild(fragment);
        </script>
    </body>
</html>
innerHTML

使用innerHTML與使用諸如createElement() appendChild()方法有一個顯著的區(qū)別,前者使用內(nèi)部的DOM來創(chuàng)建DOM結(jié)構(gòu)喻括,后者使用JavaScript的DOM來創(chuàng)建DOM結(jié)構(gòu)邀杏,前者要快得多,之前的例子用innerHTML改寫為:

var ul = document.getElementById("ul"),
    innerHTML = "";
for(let i=0; i<10; i++) {
    innerHTML += "<li>Item " + i + "</li>";
}
ul.innerHTML = innerHTML;

整合冒泡事件處理

頁面上的事件處理程序數(shù)量與頁面相應(yīng)用戶交互的速度之間存在負(fù)相關(guān)双妨,具體原因有多方面:

  1. 創(chuàng)建函數(shù)會占用內(nèi)存
  2. 綁定事件處理方法時(shí)淮阐,需要訪問DOM

因此對于冒泡事件,盡可能由父元素甚至祖先元素代子元素處理刁品,這樣一個事件處理方法可以負(fù)責(zé)多個目標(biāo)的事件處理泣特,比如:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <div id="container" style="with: 100px; height: 100px; border: 1px solid black;">
            <div id="child">this</div>
        </div>
        <script>
            var container = document.getElementById("container");
            container.addEventListener("click", function(e) {
                switch(e.target.id) {
                    case "container":
                        console.log("container clicked");
                        break;
                    case "child":
                        console.log("child clicked");
                        break;
                }
            },false);
        </script>
    </body>
</html>

注意HTMLCollection

訪問HTMLCollection的代價(jià)非常昂貴。
下面的每個項(xiàng)目(以及它們指定的屬性)都返回 HTMLCollection:

  1. Document (images, applets, links, forms, anchors)
  2. form (elements)
  3. map (areas)
  4. select (options)
  5. table (rows, tBodies)
  6. tableSection (rows)
  7. row (cells)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末挑随,一起剝皮案震驚了整個濱河市状您,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌兜挨,老刑警劉巖膏孟,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異拌汇,居然都是意外死亡柒桑,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進(jìn)店門噪舀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來魁淳,“玉大人,你說我怎么就攤上這事与倡〗绻洌” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵纺座,是天一觀的道長息拜。 經(jīng)常有香客問我,道長净响,這世上最難降的妖魔是什么少欺? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮馋贤,結(jié)果婚禮上狈茉,老公的妹妹穿的比我還像新娘。我一直安慰自己掸掸,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著扰付,像睡著了一般堤撵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上羽莺,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天实昨,我揣著相機(jī)與錄音,去河邊找鬼盐固。 笑死荒给,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的刁卜。 我是一名探鬼主播志电,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蛔趴!你這毒婦竟也來了挑辆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤孝情,失蹤者是張志新(化名)和其女友劉穎鱼蝉,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體箫荡,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡魁亦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了羔挡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片洁奈。...
    茶點(diǎn)故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖婉弹,靈堂內(nèi)的尸體忽然破棺而出睬魂,到底是詐尸還是另有隱情,我是刑警寧澤镀赌,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布氯哮,位于F島的核電站,受9級特大地震影響商佛,放射性物質(zhì)發(fā)生泄漏喉钢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一良姆、第九天 我趴在偏房一處隱蔽的房頂上張望肠虽。 院中可真熱鬧,春花似錦玛追、人聲如沸税课。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽韩玩。三九已至垒玲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間找颓,已是汗流浹背合愈。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留击狮,地道東北人佛析。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像彪蓬,于是被迫代替她去往敵國和親寸莫。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容