寫於 2017.02.16
入手《高效能JavaScript》一週後,終於斷斷續續看完了。簡要說說感受,就是這本書非常薄,非常容易看,認真看的話其實兩三個小時就能翻一遍了。這篇文章也是作為一篇閱讀筆記,用來記錄我在閱讀過程中的一些理解與感悟。
乍一看上去,這本書裡面有相當一部分內容是非常舊的,很多優化手段在如今高速的網路環境以及先進的瀏覽器的加持下,視乎已經失去了必要性。然而作為一個有潔癖的人,是無法允許自己的程式碼“大概差不多就好”,而且我也相信任何一個有追求的人都希望自己的作品是精益求精的,所以這本書仍然有非常大的學習意義。拋開主觀,在閱讀一些優秀開源庫時,看到別人的某些程式碼非常不理解,在看完這本書以後回想起來才發出感慨,原來人家這麼做的目的是為了提升效能。
全書共分為十章內容,以下將按照書本章節的順序,來逐一撰寫我的閱讀筆記。
一、載入和執行
把js放在</body>
結束標籤之前而不是<head></head>
標籤內部能夠避免瀏覽器阻塞,提升使用者體驗,已經算是一個常識。這個常識的背後,涉及到了瀏覽器單程式的概念。
事實上,多數瀏覽器使用單一程式來處理使用者介面(UI)重新整理和javascript指令碼執行,所以同一時刻只能做一件事。
這裡說的使用者介面重新整理,指的是我們“所能看到的UI”變化(比如點選一個按鈕,會出現按鈕被按下去的效果)。換句話來說,處理UI就無法處理javascript,反之亦然。所以如果一份執行時間很長的js指令碼放在頁面頂端,會阻塞之後頁面的下載和渲染,給使用者的感覺就是“頁面一片空白卡死不會動”。
雖然現在網速和瀏覽器的效率已經得到了巨大的提升,但隨著移動端的興起以及前端框架如Vue
,React
的大量使用,這個問題還是非常值得我們注意的。
二、資料存取
首先對於資料的存取,有以下這麼一句關鍵:
每一個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》這本書非常精緻,內容也非常豐富。這篇讀書筆記僅僅作為首次閱讀草草而作的讀書筆記,對於書中內容的理解或多或少都會有失偏頗,如果發現有錯漏或者更好的理解方式,歡迎留言和我交流~