《高效能JavaScript》讀書筆記

Jrain發表於2019-01-14

寫於 2017.02.16

《高效能JavaScript》讀書筆記

入手《高效能JavaScript》一週後,終於斷斷續續看完了。簡要說說感受,就是這本書非常薄,非常容易看,認真看的話其實兩三個小時就能翻一遍了。這篇文章也是作為一篇閱讀筆記,用來記錄我在閱讀過程中的一些理解與感悟。

乍一看上去,這本書裡面有相當一部分內容是非常舊的,很多優化手段在如今高速的網路環境以及先進的瀏覽器的加持下,視乎已經失去了必要性。然而作為一個有潔癖的人,是無法允許自己的程式碼“大概差不多就好”,而且我也相信任何一個有追求的人都希望自己的作品是精益求精的,所以這本書仍然有非常大的學習意義。拋開主觀,在閱讀一些優秀開源庫時,看到別人的某些程式碼非常不理解,在看完這本書以後回想起來才發出感慨,原來人家這麼做的目的是為了提升效能。

全書共分為十章內容,以下將按照書本章節的順序,來逐一撰寫我的閱讀筆記。

一、載入和執行

把js放在</body>結束標籤之前而不是<head></head>標籤內部能夠避免瀏覽器阻塞,提升使用者體驗,已經算是一個常識。這個常識的背後,涉及到了瀏覽器單程式的概念。

事實上,多數瀏覽器使用單一程式來處理使用者介面(UI)重新整理和javascript指令碼執行,所以同一時刻只能做一件事。

這裡說的使用者介面重新整理,指的是我們“所能看到的UI”變化(比如點選一個按鈕,會出現按鈕被按下去的效果)。換句話來說,處理UI就無法處理javascript,反之亦然。所以如果一份執行時間很長的js指令碼放在頁面頂端,會阻塞之後頁面的下載和渲染,給使用者的感覺就是“頁面一片空白卡死不會動”。

雖然現在網速和瀏覽器的效率已經得到了巨大的提升,但隨著移動端的興起以及前端框架如VueReact的大量使用,這個問題還是非常值得我們注意的。

二、資料存取

首先對於資料的存取,有以下這麼一句關鍵:

每一個js函式都會帶有一個叫做[[Scope]]的內部屬性,也就是該函式的作用域鏈,它決定了哪些資料能被函式訪問。

書上詳細介紹了作用域鏈執行上下文活動物件全域性物件閉包等概念,在這裡就不進行復述了。用我自己理解的話來說,就是一個函式若要使用一個變數,它會從最近的地方,也就是定義在函式內部的區域性變數裡面去找;若沒有找到,則往更遠處的全域性變數(或者上一級作用域)裡面去找。恰恰是這個“找”的過程,產生了效能的問題。書上使用了“解析識別符號”來表述“找”這個動作,而js效能恰恰是隨著解析識別符號深度的增加而降低,所以在最佳實踐裡,往往是通過把一個較深的變數賦值給一個區域性變數,在函式內部直接呼叫這個區域性變數來提升效能。

說完變數,就到了方法。在js中一切皆物件,然而js的物件是基於原型而來,這就引出了一個原型鏈的概念。與前文關於解析識別符號的原理類似,要呼叫一個物件中的方法,首先會從這個物件例項中查詢,若找不到,則會沿著其原型鏈一步一步由近到遠地往上查詢,其效能也是隨之下降的。

另外,書上也討論到了關於“巢狀成員”的問題。比如window.location.href,它會先找到window物件,然後查詢巢狀於內的location物件,再找到這裡面的href屬性,前前後後套了多層,在效能上也有著一定的花銷。所以在實際的編碼過程中,我們更多時候會面對的往往是這種巢狀成員的問題,時刻記得快取物件成員的值,在執行完畢後利用cacheObj = null的方式釋放快取,可以有效地提高效能,如下例子:

// bad
document.querySelector('.xxx').style.margin = 10 + 'px'
document.querySelector('.xxx').style.padding = 10 + 'px'
document.querySelector('.xxx').style.color = 'pink'

// good
let xxxStyle = document.querySelector('.xxx').style
xxxStyle.margin = 10 + 'px'
xxxStyle.padding = 10 + 'px'
xxxStyle.color = 'pink'
xxxStyle = null
複製程式碼

三、瀏覽器中的DOM

這一章節詳細介紹了關於dom操作的一系列問題。首先要明確一個知識點就是dom操作是具有“天生就慢”的問題。為什麼會如此呢?因為在瀏覽器裡面,處理html和js是兩套不同的機制,他們通過介面來進行聯絡的。引用書中的原話,就是可以把html和js理解為兩座島,他們之間需要一座橋來進行溝通,而過橋則會產生時間與成本上面的開銷,也因此引起了效能的問題。這一章節通過分析不同的dom操作函式,來綜合對比了各種方法的速度。

dom操作往往容易引起瀏覽器的重繪與重排。重排,指的是頁面的佈局和幾何屬性改變時所發生的事情;重繪,是指把dom元素繪製到螢幕上面的過程。

會造成效能問題的,往往來自於重排,因為瀏覽器需要重新計算頁面所有元素的大小與位置,然後把它們安置在正確的地方。所以,要提升頁面的效能,很重要的一個舉措就是避免頁面的重排。

值得注意的是,並非只有在修改頁面元素的大小和位置的時候才會引發重排,在獲取的時候瀏覽器也會出發重排,以返回正確的值。

然而很多時候我們不得不直接操作dom,儘管它們會引起重排和重繪。書上給出了幾個方案,都能有效提升效能。其實方法和上文關於js快取區域性變數的方法類似,也是通過快取的機制,減少對於dom元素屬性的查詢,以及批量修改變數再一次性更新dom的辦法去減少查詢與修改。

除此以外,讓元素脫離文件流也是一個很好的方法。因為元素一旦脫離文件流,它對其他元素的影響幾乎為零,效能的損耗就能夠有效侷限於一個較小的範圍。

講完重排與重繪,往dom元素上繫結事件也是引起效能問題的元凶。利用瀏覽器自帶的冒泡或捕獲機制,可以通過事件委託的方式減少事件處理器的數量,從而把效能優化得更好。

四、演算法和流程控制

這一章首先分析了幾種迴圈型別,結論是隻有for-in迴圈的效能最慢,因為每次迭代都會同事搜尋例項或原型屬性,導致其效能只有其它型別速度的1/7。 迴圈在程式碼中非常常見,既然無法避免,則需要通過儘量減少迴圈次數,減輕每次迴圈的工作量的方式提升效能。

對於條件語句if else或者switch,其效能在現實中並沒有太大區別,關鍵是要正確處理語義化的需求。有的時候也可以使用查表法進行。

對於遞迴演算法,最好的提升效能方法是快取上次執行的結果,在下一次遞迴的時候直接引用而非從頭開始計算。

五、字串和正規表示式

TODO…… (對於正規表示式還沒有特別熟悉,這一章跳著看了)

六、快速響應的使用者介面

前面五章都是針對JS原生的語法分析效能問題,從這一章開始分析針對使用者介面的可感知效能問題。

由於瀏覽器是單執行緒運作的,在處理UI事件的時候無法處理js事件,反之亦然,所以對於耗時過長的js任務來說,可以使用定時器的方法使其讓出執行緒控制權,讓瀏覽器優先處理UI事件以提升使用者體驗。

html5新增的web worker允許多開執行緒,意味著耗時較長、效能損耗較大的js任務可以放到web worker中進行,而無需阻塞瀏覽器UI執行緒的執行。值得注意的是,web worker無法使用瀏覽器相關的資源,所以無法用以進行dom操作等。

七、Ajax

ajax技術已經是如今的主流技術,在這裡就無需贅述了。書上關於其效能優化的內容,多集中在瀏覽器資源快取上。如果能夠有效利用瀏覽器的快取機制,可以大大減少與服務端的互動,提升效能。

書上沒有提及的是現在逐漸開始流行的fetch API,關於這方面的效能的問題也值得我們研究。

其他

剩下的內容都是一些程式設計實踐,程式碼優化等等。

在如今的前端開發領域中,上線的程式碼一般都會經過程式碼合併、壓縮,服務端開啟gzip等工作。隨著http2的發展,網頁的效能更會得到提高,可能傳統”檔案合併“這一工作會逐漸被摒棄。另外http2的服務端推送也能極大地提升頁面載入速度,這部分內容在我另外一篇文章《深入研究:HTTP2 的真正效能到底如何》有詳細研究,有興趣的讀者可以去看看。

《高效能JavaScript》這本書非常精緻,內容也非常豐富。這篇讀書筆記僅僅作為首次閱讀草草而作的讀書筆記,對於書中內容的理解或多或少都會有失偏頗,如果發現有錯漏或者更好的理解方式,歡迎留言和我交流~

相關文章