10年前,亞馬遜分享一個例子座咆,每100毫秒的延遲都會是他們損失1%的銷售收入痢艺,即在全年中,每增加1秒鐘的加載時間將使該公司損失約16億美元介陶。同樣堤舒,谷歌發(fā)現(xiàn)搜索頁面的生成時間增加500毫秒,訪問量將減少20%哺呜,潛在的廣告收入也將減少五分之一舌缤。
我們中很少人可以像谷歌和亞馬遜一樣去處理這種大場面,但是某残,相同的原則也適用于更小規(guī)模的場景国撵,速度更快的代碼可以帶來更好的用戶體驗,并且對業(yè)務(wù)更有利玻墅。特別是在web開發(fā)中介牙,速度可能在與對手競爭中成為關(guān)鍵因素。在較快的網(wǎng)絡(luò)上浪費的每一個毫秒澳厢,就會在較慢的網(wǎng)絡(luò)上放大耻瑟。
在本文中,我們將探討13種提升JavaScript運行速度的實用技巧赏酥,無論你是寫基于Node.js的服務(wù)端代碼還是客戶端的JavaScript代碼。我已經(jīng)提供了基于https://jsperf.com創(chuàng)建的性能測試用例谆构。如果你想自己測試這些技巧裸扶,請確保點擊這些鏈接。
Do It Less
最快的代碼是那些從來不會運行的代碼搬素。
1. 刪除無用的功能
開始著手優(yōu)化已經(jīng)寫好的代碼是一件很容易的事情呵晨,但是魏保,對性能提升最大的方法往往來自于退后一步問問自己為什么我們的代碼需要出現(xiàn)在這里。
在繼續(xù)某項優(yōu)化工作之前摸屠,問問自己你的代碼是否真的需要做他現(xiàn)在所做的事情谓罗。這個功能里面的組件或者函數(shù)是否有必要?如果沒有季二,請刪掉它檩咱。這一步對提升代碼速度非常重要,卻很容易被忽略胯舷。
2. 避免無用的步驟
基準(zhǔn)測試:https://jsperf.com/unnecessary-steps
在較小的規(guī)模上刻蚯,一個函數(shù)運行過程中執(zhí)行的每一步都有用么?舉個例子桑嘶,為了達到最終的效果炊汹,你的數(shù)據(jù)是否會陷于一個沒有必要的圈中?下面的示例可能被簡化了逃顶,但是讨便,它能代表那些在較大代碼量中很難被發(fā)現(xiàn)的問題。
'incorrect'.split('').slice(2).join(''); // converts to an array
'incorrect'.slice(2); // remains a string
即使在簡單的例子中以政,性能上的差異也是十分巨大的霸褒,運行某些代碼比不運行任何代碼要慢得多!盡管很少有人會犯上述錯妙蔗,但是面對更長傲霸、更復(fù)雜的代碼,在獲取結(jié)果的前加上一些毫無價值的步驟就會很容易眉反。盡量避免它昙啄!
Do It Less often
如果你不能刪除代碼,問問你自己能不能減少做這件事情的頻率呢寸五?代碼如此強大的原因之一是他可以使用我們輕松的完成重復(fù)的操作梳凛,但是,也更容易讓我們的代碼執(zhí)行次數(shù)超過需要的次數(shù)梳杏。以下是一些需要注意的特殊情況韧拒。
3. 越早退出循環(huán)越好
基準(zhǔn)測試:https://jsperf.com/break-loops/1
在一個循環(huán)中找出不需要迭代完成的情況。舉個例子十性,如果你正在尋找一個特殊值并且已經(jīng)找到他了叛溢,那么剩下的迭代就已經(jīng)不需要了。你應(yīng)該通過使用break
語句來中斷正在執(zhí)行中的循環(huán):
for (let i = 0; i < haystack.length; i++) {
if (haystack[i] === needle) {
break;
}
}
或者劲适,如果楷掉,你需要只對循環(huán)中某些元素做操作時,你可以使用continue
語句來跳過對其他元素進行操作霞势。continue
會終止當(dāng)前迭代中的執(zhí)行語句烹植,立即跳轉(zhuǎn)到下一個語句中:
for (let i = 0; i < haystack.length; i++) {
if (!haystack[i] === needle) {
continue;
}
doSomething();
}
值得注意的是斑鸦,你也可以通過break
或continue
跳過嵌套的循環(huán):
loop1: for (let i = 0; i < haystacks.length; i++) {
loop2: for (let j = 0; j < haystacks[i].length; j++) {
if (haystacks[i][j] === needle) {
break loop1;
}
}
}
4. 僅僅初始化一次
基準(zhǔn)測試:https://jsperf.com/pre-compute-once-only/6 (譯者在mac下自測,使用/不使用閉包草雕,使用/不使用全局變量巷屿,目前對性能影響差異不大)
在我們的應(yīng)用中,我們將會調(diào)用數(shù)次下列方法:
function whichSideOfTheForce1(name) {
const light = ['Luke', 'Obi-Wan', 'Yoda'];
const dark = ['Vader', 'Palpatine'];
return light.includes(name) ? 'light' : dark.includes(name) ? 'dark' : 'unknown';
}
whichSideOfTheForce1('Luke');
whichSideOfTheForce1('Vader');
whichSideOfTheForce1('Anakin');
這段代碼墩虹,我們每次調(diào)用whichSideOfTheForce1
時嘱巾,都會重新創(chuàng)建2次數(shù)組,每次調(diào)用時都需要給我們的數(shù)組重新分配內(nèi)存败晴。
提供的數(shù)組的值是固定的浓冒,那最好的解決辦法就是定義一次,然后在函數(shù)中調(diào)用它的引用尖坤。盡管我們也可以全局定義這2個數(shù)組變量稳懒,但是,這將允許他們在我們的函數(shù)外部被篡改慢味。最好的解決方法是使用閉包场梆,這就意味著他返回的是一個函數(shù):
function whichSideOfTheForceClosure1(name) {
const light = ['Luke', 'Obi-Wan', 'Yoda'];
const dark = ['Vader', 'Palpatine'];
return (name) => (light.includes(name) ? 'light' : dark.includes(name) ? 'dark' : 'unknown');
}
const whichSideOfTheForce2 = whichSideOfTheForceClosure1();
現(xiàn)在,我們的數(shù)組只會初始化一次了纯路。再來看看下面的例子:
function doSomething(arg1, arg2) {
function doSomethingElse(arg) {
return process(arg);
};
return doSomethingElse(arg1) + doSomethingElse(arg2);
}
每次運行doSomething
時或油,都會從頭開始創(chuàng)建嵌套函數(shù)doSomethingElse
。 閉包提供了解決方案驰唬, 如果我們返回一個函數(shù)顶岸,doSomethingElse
仍然是私有的,但只會創(chuàng)建一次:
function doSomething(arg1, arg2) {
function doSomethingElse(arg) {
return process(arg);
};
return (arg1, arg2) => doSomethingElse(arg1) + doSomethingElse(arg2);
}
5. 控制代碼的執(zhí)行順序來保證最小的運行次數(shù)
基準(zhǔn)測試: https://jsperf.com/choosing-the-best-order/1
如果仔細(xì)考慮函數(shù)中每一步的執(zhí)行順序叫编,也可以幫助我們提高代碼的執(zhí)行效率辖佣。假設(shè),我們有一個數(shù)組來存儲上平的價格(美分)搓逾,我們需要一個函數(shù)對商品的價格進行求和并返回結(jié)果(美元):
const cents = [2305, 4150, 5725, 2544, 1900];
這個函數(shù)有2件事情要做卷谈,轉(zhuǎn)化單位和求和,但是這些動作的順序很重要霞篡。如果優(yōu)先處理轉(zhuǎn)化單位世蔗,我們函數(shù)是這樣的:
function sumCents(array) {
return '$' + array.map(el => el / 100).reduce((x, y) => x + y);
}
在這個方法中,我們對數(shù)組的每一項都需要進行除法朗兵,如果改變執(zhí)行的順序污淋,我們只需要進行一次除法:
function sumCents(array) {
return '$' + array.reduce((x, y) => x + y) / 100;
}
優(yōu)化性能的關(guān)鍵就是確保函數(shù)以最佳的順序執(zhí)行。
6. 了解代碼的時間復(fù)雜度 O(n)
了解代碼的時間復(fù)雜度是理解為什么某些方法比其他方法運行的更快余掖,占用的內(nèi)存更少的最佳方法之一寸爆。例如,你可以通過使用時間復(fù)雜一目了然的了解為什么二分搜索是效率最好的搜索算法之一,為什么快排是往往是最有效的排序算法而昨。詳細(xì)請自行了解時間復(fù)雜度
Do It Faster
代碼速度優(yōu)化收益最大的往往是前面2類。在本節(jié)中我們將討論提高代碼速度的幾種方法找田,他們更多的是和代碼優(yōu)化相關(guān)歌憨,而不是剔除他或者減少運行的次數(shù)。
當(dāng)然墩衙,這些優(yōu)化也要減少代碼的大小或者使其對編譯器更友好务嫡,但是,表面上看你只更改了代碼而已漆改。
7. 多用內(nèi)置函數(shù)
基準(zhǔn)測試:https://jsperf.com/prefer-built-in-methods/1
對于擁有編譯器和底層語言經(jīng)驗的人來說心铃,這是一件很明顯的事情。但是挫剑,這里還是要把它作為一個基礎(chǔ)規(guī)則來提一下去扣,如果JavaScript有內(nèi)置函數(shù),請使用它樊破。
編譯器代碼在設(shè)計時愉棱,就針對方法或者對象類型進行了性能優(yōu)化。另外哲戚,內(nèi)置方法的底層語言是C++奔滑。除非你的用例特別具體,否則顺少,你自己的JavaScript代碼很少能比現(xiàn)有內(nèi)置代碼快朋其。
為了測試這個,我們自己來實現(xiàn)一個map方法
function map(arr, func) {
const mapArr = [];
for (let i = 0; i < arr.length; i++) {
const result = func(arr[i], i, arr);
mapArr.push(result);
}
return mapArr;
}
讓我們來創(chuàng)建一個數(shù)組脆炎,里面包含了100個隨機數(shù)字(1-100)梅猿。
const arr = [...Array(100)].map(e=>~~(Math.random()*100));
我們來執(zhí)行一些簡單操作(數(shù)字乘2)來比較二者的差異:
map(arr, el => el * 2); // Our JavaScript implementation
arr.map(el => el * 2); // The built-in map method
在我的測試中,我們自己實現(xiàn)的map
方法比原生的Array.prototype.map
慢65%腕窥。
8. 選擇最佳的數(shù)據(jù)類型
基準(zhǔn)測試1:set.add()
vs array.push()
https://jsperf.com/adding-to-a-set-vs-pushing-to-an-array
基準(zhǔn)測試2:map.set()
vs object['xx']
https://jsperf.com/adding-map-vs-adding-object
同樣粒没,最佳的性能也可能來自于選擇合適的內(nèi)置數(shù)據(jù)類型。JavaScript中內(nèi)置的數(shù)據(jù)類型遠(yuǎn)遠(yuǎn)不止:Number
簇爆、String
癞松、Function
、Object
入蛆。很多不常見的數(shù)據(jù)類型如果在正確的場景中使用將會提供非常明顯的優(yōu)勢响蓉。
Set
和Map
在頻繁添加和刪除元素的情況下有明顯的性能優(yōu)勢。
了解內(nèi)置的對象類型哨毁,并嘗試使用最適合你需要的對象類型枫甲,這對提升代碼的性能非常有用。
9. 別忘了內(nèi)存
JavaScript作為一種高級語言,它為你處理很多底層細(xì)節(jié)想幻。內(nèi)存管理就是其中一個粱栖。JavaScript使用一種稱為垃圾回收(GC)的系統(tǒng)來釋放內(nèi)存,在不需要開發(fā)人員明確指示的情況下脏毯,就可以自動釋放內(nèi)存闹究。
盡管內(nèi)存管理在JavaScript中是自動的,但這并不意味著它是完美的食店。你也可以采取其他步驟來管理內(nèi)存并減少內(nèi)存泄漏的機會渣淤。
例如,Set
和Map
有變體WeakSet
和WeakMap
吉嫩,他們持有對象的“弱”引用价认。他們通過確保其中的對象沒有其他對象引用時觸發(fā)垃圾回收,來確保不會出現(xiàn)內(nèi)存泄漏自娩。
在ES2017之后用踩,你可以通過TypedArray
對象來更好的控制內(nèi)存的分配。例如椒功,Int8Array
可以放-128到127之間的值捶箱,僅僅占用一個字節(jié)。但是动漾,值得注意的是丁屎,使用TypedArray
的性能提升可能很小:將常規(guī)數(shù)組與Uint32Array進行比較寫入性能略有改善旱眯,但讀取性能卻幾乎沒有改善晨川。
對于底層編程語言有基本的了解可以幫助你編寫更快、更好的JavaScript代碼删豺。
10. 盡可能使用單態(tài)
基準(zhǔn)測試1:https://jsperf.com/monomorphic-forms
基準(zhǔn)測試2:https://jsperf.com/impact-of-function-arguments
如果我們設(shè)置const a = 2
共虑,則變量a可以被視為多態(tài)的(可以更改)。 相反呀页,如果我們直接使用2妈拌,則可以認(rèn)為是單態(tài)的(其值是固定的)。
當(dāng)然蓬蝶,如果我們需要多次使用變量尘分,則設(shè)置變量非常有用。 但是丸氛,如果你只使用一次變量培愁,則完全避免設(shè)置變量會稍快一些。 采取簡單的乘法功能:
// 函數(shù)定義
function multiply(x, y) {
return x * y;
}
如果我們運行multiply(2, 3)
缓窜,他比直接運行下面的代碼快1%:
// 定義2個變量作為multiply的參數(shù)
let x = 2, y = 3;
multiply(x, y);
這是一個小勝利定续,在大型代碼中谍咆,性能提升往往是由大量小勝利組成的。
同樣私股,在函數(shù)中使用參數(shù)可提供靈活性摹察,但會降低性能。 如果不需要它們倡鲸,就可以把它變成一個常量放在函數(shù)中港粱,它會略微提高性能。因此旦签,multiply
的更快版本如下所示:
// 如果3是固定不變的時候,則直接作為函數(shù)中的一部分
function multiplyBy3(x) {
return x * 3;
}
結(jié)合上述優(yōu)化寸宏,在我的測試中性能提升約為2%宁炫。雖然改動的點比較小,但是如果可以在大型代碼庫中多次進行這種改進氮凝,就值得考慮了羔巢。
譯者注,這里原文太繞了罩阵,大家看看代碼里面的注釋理解一下
a. 僅在值必須是動態(tài)的時才引入函數(shù)參數(shù)竿秆,否則,就寫成函數(shù)內(nèi)部的變量稿壁;
b. 僅在多次使用某一個值時才引入變量幽钢,否則,就直接寫值傅是;
11. 避免使用delete
基準(zhǔn)測試1: https://jsperf.com/removing-variables-from-an-object/1
基準(zhǔn)測試2: https://jsperf.com/delete-vs-map-prototype-delete
delete
關(guān)鍵詞的作用是用來刪除對象中的某一個屬性匪燕。也許你會覺得這個對于你的應(yīng)用來說很有用,但是喧笔,希望你盡量別去用它帽驯。在v8引擎中,delete
關(guān)鍵詞消除了hidden class
的優(yōu)勢书闸,讓對象變成了一個"慢對象"尼变。
hidden class:由于 JavaScript 是一種動態(tài)編程語言,屬性可進行動態(tài)的添加和刪除浆劲,這意味著一個對象的屬性是可變的嫌术,大多數(shù)的 JavaScript 引擎(V8)為了跟蹤對象和變量的類型引入了隱藏類的概念。在運行時 V8 會創(chuàng)建隱藏類梳侨,這些類附加到每個對象上蛉威,以跟蹤其形狀/布局。這樣可以優(yōu)化屬性訪問時間
根據(jù)你的需求走哺,可能僅僅將不需要的屬性設(shè)置成undefined
就夠了蚯嫌。
const obj = { a: 1, b: 2, c: 3 };
obj.a = undefined;
我在網(wǎng)上看過一些建議哲虾,他們使用以下的功能去拷貝除去指定屬性之外的對象:
const obj = { a: 1, b: 2, c: 3 };
const omit = (prop, { [prop]: _, ...rest }) => rest;
const newObj = omit('a', obj);
但是,在我的測試中择示,上面的函數(shù)比delete
關(guān)鍵詞還要慢束凑。另外,它的可讀性也很低栅盲。
或者汪诉,你可以考慮使用Map
而不是Object
,因為谈秫,Map.prototype.delete
比delete
也快很多扒寄。
Do It Later
如果你做不到上述3個方面的優(yōu)化,你也可以試一試第四類優(yōu)化拟烫,即使運行時間完全相同也會讓你覺代碼更快该编。這涉及重構(gòu)代碼,使整體性較小或要求較高的任務(wù)不會阻塞最重要的代碼執(zhí)行硕淑。
12. 使用異步代碼避免線程堵塞
默認(rèn)情況下课竣,JavaScript是單線程的,并且會同步的執(zhí)行代碼置媳。(實際上于樟,瀏覽器代碼可能正在運行多個線程來捕獲事件并觸發(fā)處理程序,但就編寫JavaScript代碼而言拇囊,它是單線程的)
同步執(zhí)行對大多是JavaScript代碼都適用迂曲,但是,如果我們需要執(zhí)行的代碼需要很長時間寥袭,但是昔逗,我們又不想堵塞其他更重要的代碼執(zhí)行晌缘。
我們就需要使用異步代碼肴捉。像fetch()
或者XMLHttpRequest()
這些內(nèi)置方法強制是異步執(zhí)行的核蘸。值得注意的是,任何同步函數(shù)都可以異步化:如果你在執(zhí)行耗時的同步操作尝江,例如對大型數(shù)組中的每個項目執(zhí)行操作涉波,則可以使此代碼異步化,這樣就不會阻止其他代碼的執(zhí)行炭序。
此外啤覆,在NodeJs中,很多模塊都存在同步方法和異步方法兩種惭聂,例如窗声,fs.writeFile()
和fs.writeFileSync()
。在正常情況下辜纲,請默認(rèn)使用異步方法笨觅。
13. 使用Code Split
如果你在瀏覽器中寫JavaScript拦耐,那么你應(yīng)該優(yōu)先確保你的頁面展示的越快越好〖#“首屏渲染”是一個衡量瀏覽器渲染第一個有效界面時間的關(guān)鍵指標(biāo)杀糯。
改善此問題的最佳方法就是通過JavaScript代碼拆分。與其將所有代碼都打包在一起苍苞,不如將其拆分成較小的塊固翰,這樣就可以預(yù)加載更少的JavaScript代碼。根據(jù)你是用的引擎不同羹呵,代碼拆分的方法也不同骂际。
Tree-Shaking
是一個從代碼庫中剔除無用的代碼的策略,你可以通過這篇文章tree-shaking來了解他冈欢。
總結(jié)
確保你的優(yōu)化策略有效的最佳方案就是測試他們方援,我在文章中使用測試性能,你也可以試一試:
- http://jsben.ch/
- https://jsbench.me/
-
console.time
和consoele.timeEnd
Chrome的開發(fā)者工具中的性能和網(wǎng)絡(luò)面板是檢查web應(yīng)用的性能的好工具涛癌,同時,我還推薦使用Google的LightHouse送火。
最后拳话,盡管速度很重要,但是种吸,速度并不是好代碼的全部弃衍。可讀性和可維護性也很重要坚俗,如果為了提升輕微的性能而導(dǎo)致需要花更多的時間去找BUG并修復(fù)它镜盯,事情將變得很不值得。
關(guān)于我
我是一個莫得感情的代碼搬運工猖败,每周會更新1至2篇前端相關(guān)的文章速缆,有興趣的老鐵可以掃描下面的二維碼關(guān)注或者直接微信搜索前端補習(xí)班
關(guān)注。
精通前端很難恩闻,讓我們來一起補補課吧艺糜!
好啦,翻譯完畢啦幢尚,原文鏈接在此 13 Tips to Write Faster, Better-Optimized JavaScript破停。