JS 是一門弱類型語言,擁有獨特的原型鏈機制,在宿主中的擁有一套 DOM、BOM 操作接口,增加其性能控制的復雜性。JavaScript 主要應用場景依然圍繞瀏覽器展開,所以,它在瀏覽器中的行為表現(xiàn)依然重要。本篇將從筆者的實踐經(jīng)驗出發(fā),分別從加載解析、語法優(yōu)化、DOM 操作等各方面歸納總結(jié)優(yōu)秀的 JS 代碼性能優(yōu)化策略。與此同時,關(guān)注如何編寫更優(yōu)雅干凈的 JS 代碼。
加載解析
JS 文件的加載解析涉及到瀏覽器對于文檔的解析和渲染策略,一些不好的文檔結(jié)構(gòu),會導致渲染空屏、卡頓,甚至出現(xiàn)頁面機能混亂等問題。對于 JS 來說,在加載解析階段可以從以下幾個方面作出優(yōu)化。
- 將 JS 文件放到文檔最后(</body>之前)引入
JS 代碼通過 <script> 標簽引入頁面加載,<script> 標簽是一個霸道的主兒,當文檔遇到它時,會暫停解析,等待它執(zhí)行完畢,再繼續(xù)解析剩余的部分。在新瀏覽器中,多個 <script> 標簽的內(nèi)容彼此不會阻塞,可以并行下載,但其他資源仍然會被阻塞,所以,將 JS 文件放到文檔最后引入,仍是最有效的優(yōu)化策略。
- 合并 JS 文件
減少 HTTP 請求是最常見的性能優(yōu)化策略,引入四個 10 kb 的文件需要做四次請求,要比引入一個 40kb 更耗性能,所以,當需要引入的 JS 文件過多時,必要的腳本合并是很有必要的。
但需要注意的是,如果一個文件過大,其解析的用時將會很大,這樣無疑是得不償失的,所以,不要有的沒的全懟在一起。
- 使用 defer 無阻塞下載腳本
defer 是標準中為 <script> 標簽提供的一個屬性,其會使 JS 文件的下載和文檔的渲染并行展開,同時延遲 JS 的執(zhí)行時間到文檔加載完畢,所以這個屬性十分有用。
順便提一下另一個可以應用在 <script> 上的屬性 async,顧名思義,這個屬性的作用是,使得腳本的加載和執(zhí)行與文檔的渲染并行進行。它與 defer 在下載腳本的時機是一致的,只不過,執(zhí)行時機不同。下面這張圖很形象地表明了這兩個屬性以及不帶屬性的腳本加載執(zhí)行機制。

語法優(yōu)化
- 慎用全局變量
全局變量的查找作用域鏈更長,生命周期也更長,且有內(nèi)存泄漏的風險,甚至會產(chǎn)生不可預估的 bug 出現(xiàn)。項目中的第三方庫一般都會暴露一些全局變量,這和你聲明的全局變量也可能會發(fā)生沖突。所以,盡量謹慎地使用全局變量。
在 ES6 之后,我們使用 let/const 來聲明變量是更好的選擇。
- 使用性能更優(yōu)的遍歷操作
經(jīng)過測試,JS 中的循環(huán)操作,耗時從小到大排名為:for -> forEach/for-of -> for-in
也就是說,對于大規(guī)模的遍歷操作,優(yōu)先使用 for 循環(huán)完成,其次是 forEach 以及 for-of,這兩者處于一個數(shù)量級,最慢的屬于 for-in,它之所以慢,是因為它常用于對象屬性的遍歷,并且會訪問自身屬性以及其原型鏈的屬性(包括不可枚舉屬性)。
- 避免使用 with 和 eval
with 可以改變當前的作用域環(huán)境,將一個對象推入作用域鏈頭部,這樣,使得作用域環(huán)境內(nèi)的局部變量的訪問效率降低。
eval 將傳入的字符串當做腳本執(zhí)行,會大幅度降低腳本執(zhí)行性能,避免使用。
- 盡量少地使用閉包
閉包提供了一些便捷性,但同時也會有一些性能影響,由于保留著原應被回收的變量引用,增加了作用域鏈的長度,影響性能。
同時它會可能會有內(nèi)存泄漏的風險。
- 不要修改引用類型的原型方法
修改原型方法,在團隊協(xié)作中,很可能帶來不可預估的影響,盡量避免這樣做。
- 當判斷值很多時,優(yōu)先考慮 switch 替代 if-else
當判斷條件過多,例如超過3個,就應該考慮使用 switch 來替換 if-else。這樣,不止可以提高代碼的可讀性,降低代碼的理解成本。
對于 if 語句的優(yōu)化,還有一些策略:提前 return;使用三元操作符;借用 ES6 的 Map 結(jié)構(gòu)進行優(yōu)化等,感興趣的同學可以閱讀這篇文章:https://juejin.im/post/5bdfef86e51d453bf8051bf8
- 避免在循環(huán)中創(chuàng)建函數(shù)
每次循環(huán)創(chuàng)建一個函數(shù)不是明智之舉,創(chuàng)建函數(shù)意味著內(nèi)存分配與消耗,這是無用功,應該提前創(chuàng)建函數(shù)。
- 總是使用 === 和 !== 進行類型判斷
== 和 != 操作符會引起 JS 的數(shù)據(jù)類型隱式轉(zhuǎn)換,導致一些不可預估的負面作用,所以,更明智的選擇是,總是去使用 === 和 !== 進行相等判斷。
- 使用字面量新建對象
通過 new 操作符新建一個對象,類似于函數(shù)調(diào)用,同時會做一些關(guān)聯(lián)原型鏈等操作,性能會慢很多,字面量則在寫法上更直觀友好且高效。
新建數(shù)組類似。
- 不要省略花括號
很多同學喜歡省略條件判斷語句后面的花括號,像下面這樣:
if (somethingIsTrue)
a = 100
doSomething()
這樣的代碼,你的目的可能是這樣的效果:
if (somethingIsTrue) {
a = 100
doSomething()
}
但其實它會按這樣執(zhí)行:
if (somethingIsTrue) {
a = 100
}
doSomething()
所以,還是老老實實地加上花括號,以避免上面這樣的情況。
- 要不要加分號?
近年來,加不加分號在 JS 中的討論很激烈,如果你足夠了解 JS 的解釋機制,那么你可以選擇不加分號,但是如果你僅僅是為了少寫幾個字符,我認為還是加上分號比較好。
注:主要有以下幾個字符會引起 JS 上下文解析有誤:括號,方括號,正則開頭的斜杠,加號,減號。
還有一個參考標準是,這只是一個風格問題,應該根據(jù)你的項目風格而定,與團隊保持一致最好。而且,成熟的 JS 的編譯器都會判斷什么地方該加分號,所以說,不加分號出錯的概率極低,如果你能夠采取更好的換行策略,不加分號是完全沒問題的。
- 優(yōu)先使用原生方法
雖然一些諸如 lodash、jQuery 這樣的操作庫大大提升 JS 開發(fā)者的生產(chǎn)力,但是,對于原生 JS 可以實現(xiàn)的功能,使用原生 JS 一般都會獲得更快的解析速度。
例如這個例子:
$('input').on('focus', function() {
if ($(this).val() === 'some text') { ... }
})
很明顯,這里沒有必要使用 val() 方法,我們可以使用原生方法代替:
$('input').on('focus', function() {
if (this.value === 'some text') { ... }
})
DOM 操作優(yōu)化
大量的 DOM 操作會引發(fā)頁面卡頓,極耗性能,這是因為,在瀏覽器中,ECMAScript 的解釋引擎和 DOM 的渲染引擎由兩個部分實現(xiàn),例如 Chrome 的 JS 引擎為 V8,而 DOM 則是 WebCore 實現(xiàn)。而 DOM 操作,你可以理解為跨模塊操作,將 JS 和 DOM 比作兩座島嶼,而操作 DOM,就是 JS 跨過大橋,去 DOM 島上做文章,每次操作,就要過一次橋,頻繁過橋的話,會引發(fā)巨大的性能損耗(參考文末《天生就慢的DOM如何優(yōu)化?》)。
這個過橋過程,主要發(fā)生在以下的操作中:
- 訪問和修改 DOM 元素
- DOM 元素的重繪(Reflow)或重排(Repaint)
這也是為什么現(xiàn)代框架都使用 virtual DOM 的原因之一。若不使用現(xiàn)代 JS 框架,DOM 操作的優(yōu)化原則是:盡量減少過橋的次數(shù),也就是盡量少地訪問 DOM 元素,盡量減少 DOM 結(jié)構(gòu)的重繪(Reflows)或重排(Repaints)。
常用的優(yōu)化策略有:
- 最小化 DOM 訪問次數(shù)
- 合并多次 DOM 操作,一次性插入頁面
當你需要對文檔元素進行一系列操作時,應該是先將元素脫離文檔,多重操作完成后,再插入文檔(這一點經(jīng)常通過 DocumentFragment 實現(xiàn))
- 使用本地變量進行緩存頻繁訪問的 DOM 元素
- 不要遍歷 HTML 元素集合,而是將它們轉(zhuǎn)為數(shù)組之后執(zhí)行
HTML 元素集合與底層的文檔元素相關(guān)聯(lián),每次操作 HTML 元素,會引發(fā)元素集合的更新)
- 使用速度更快的 API
優(yōu)先使用 querySelectorAll() 以及 querySelector() 方法獲取元素。這兩個方法返回的節(jié)點列表,不會對應實時的文檔結(jié)構(gòu),也就避免了上一條提到的性能問題。
- 引發(fā)重排的動畫元素脫離文檔流之后再操作
動畫操作引發(fā)的重排,很可能會影響整個文檔流,引發(fā)頁面卡頓,所以,可以將發(fā)生這類動畫的元素,使用定位脫離文檔流,出發(fā) BFC,動畫完成后,回歸正常定位。
使用事件委托
試想這樣一種場景,一個 ul 中有一大堆 li,你需要為所有的 li 元素綁定點擊事件,最直觀的方法是,循環(huán)為每一個 li 綁定:
for (let i = 0; i < uls.length; i++) {
uls[i].onClick = function() {
// do something...
}
}
這種循環(huán)寫法,一方面增加了內(nèi)存開銷,另一方面,每次點擊時,增加了循環(huán)時間,損耗頁面性能。這種情況的解決辦法是:使用事件委托。
顧名思義,事件委托指的是,將事件的響應,委托到另外的元素上,一般指父元素或者上層元素。事件委托是利用 JS 的時間冒泡機制,子層的事件會向外層冒泡,所以,在事件發(fā)生元素的父元素以及更外層元素都可以監(jiān)聽到事件的發(fā)生。我們可以使用 addEventListener 來簡單實現(xiàn):
uls.addEventListener('click', function(e) {
if (e.target.tagName.toLowerCase() === 'li') {
// do something
}
})
事件委托的好處是,動態(tài)添加的元素,都可以響應到。
編寫更優(yōu)雅的 JS 代碼
程序員的工作,很大一部分并非只考慮解釋器,而是要考慮和你合作的同事,在關(guān)注準確高效的業(yè)務邏輯的同時,代碼的可讀性、干凈和優(yōu)雅,是十分重要的。
所謂干凈優(yōu)雅,我的理解是,使得讀你代碼的人可以基本不依賴注釋就可以順暢地理解你的邏輯,和寫作類似,第一要務是準確、簡潔地傳達信息。或者說,借用網(wǎng)絡上的一個說法,優(yōu)雅的代碼是自解釋的。如果你的代碼被后來者拿到,一頭霧水,懷疑人生,那就很有問題。以下是一些編寫優(yōu)雅 JS 代碼的建議:
- 使用有意義的變量名稱
這條已經(jīng)被反復提及 N 多次,但怎么強調(diào)都不為過,最基礎的部分往往是最重要的部分。好的變量名,可以大幅度提高代碼的可讀性,不需要反復通過上下文邏輯去推敲?!洞a大全》指出,好的變量名有以下的特征:
首先,它們很容易理解
好的名字應該盡可能明確。好的名字通常表達的是“什么”(what),而不是“如何”(how)。
至于具體的操作,我的建議是,打開你手頭的項目,去看看你寫下的變量名,想想有沒有優(yōu)化的地方,或者說,你自己寫的代碼,你能明確地知道眼前的變量表示什么嗎?如果不能,那就不是一個好名字。
- 使用肯定的判斷方法
以否定方法來做判斷條件,會讓人乍看過去很疑惑,例如 isNumNotValid,當其結(jié)合條件控制語句時,會大大增加閱讀負擔:
if (!isNumNotValid) { ... }
前面加上 ! 操作符后,很令人疑惑 num 到底應該是 valid 還是 not valid,應該改為 isNumValid:
if (isNumValid) { ... }
- 避免冗余的代碼
你的代碼工作區(qū)就像一個營地,你離開的時候,不應該丟下大量垃圾。冗余的代碼,主要指重復的代碼,以及不會被執(zhí)行到的代碼,例如寫在 return 語句之后的代碼,以及一些“暫時”用到的 trick,或者測試代碼,這些代碼都會大大干擾代碼的可讀性。
所以,寫代碼的人應該常常讀讀自己的代碼,看看有哪些代碼時冗余的,及時地刪除它們,并且可以采用一些策略來優(yōu)化重復的代碼,例如類的抽離,組件的抽離,模塊化,變量的緩存等等。
- 跟隨團隊的風格指南
大部分開發(fā)團隊都擁有自己的開發(fā)指南,例如業(yè)界著名的 Google、AirBnb 等都有自己的 JavaScript 指南,每個團隊都應該制定適合自己的代碼風格指南,一般包含了代碼的風格以及一些最佳的實踐策略等,按照指南的指引,勤于進行 code review,這樣,才能打造一個戰(zhàn)斗力超強的隊伍。
小結(jié)
本篇主要從代碼層面提出了一些 JavaScript 應該注意的優(yōu)化寫法,對于開發(fā)者來講,我們常常是面向項目進行編程,所以,這要求我們在深入代碼的同時,又要學會跳出來,從工程化的層面去考慮,現(xiàn)代流行的 JS 框架,正是從整體架構(gòu)的角度來優(yōu)化整個 JS 項目的寫法,在學習這些框架的時候,我們更應該去考慮 JS 底層的東西,它們到底在解決什么問題?而這些問題,很大一部分就是和這里所說的性能以及最佳實踐息息相關(guān)的,這也是開發(fā)者從一個簡單的碼農(nóng)向工程師升級的關(guān)鍵所在。
參考資料
- 吹毛求疵的追求優(yōu)雅高性能JavaScript
- 天生就慢的DOM如何優(yōu)化?
- 【書】《高性能JavaScript》