老生常談:你的 JavaScript 怎么運行得這么慢?
最近,在啃一本為人所熟知的動物書《[O`Reilly] - High Performance JavaScript - [Zakas]》。本書中,作者主要以四大角度去談及 JavaScript 如何在性能上得以提高。為了能使其變為我自身 JavaScript 知識體系中的一部分,我以自己的語言去加以總結(雖是英文,但描述得非常直白,希望大家不要害怕。)
在此,我希望通過這個 Issue,以中文的形式從更高的層次去總結所有的性能提升技巧以方便記憶與查詢(但是細節也要深入去理解與學習,而非得過且過):
1. JavaScript 的加載
作者就此角度主要談及如何去高效地加載與執行 JavaScript 代碼。正因其執行過程會產生阻塞問題,因此我們需要通過若干方式去最小化該性能影響:
- 盡可能地把所有的 script 標簽置于頁面的底部,即 body 標簽內的末尾部分。
- 使用構建工具把多個 script 組合在一起
- 使用以下地方式去異步加載 JavaScript 代碼:
- 添加 defer 屬性可預加載 JavaScript 代碼并等到頁面加載渲染完成后及 onload 事件觸發之前執行
- 以代碼的方式動態加載 script
- 通過 XMLHttpRequest 來加載 script
2. 編碼技巧
以優雅的編碼技巧去提高性能會涉及到幾個方面,如數據訪問方式、DOM 相關編碼或代碼結構等。經過此章的閱讀,個人認為這些技巧并不僅僅針對于 JavaScript 語言,而是作為一種更高層次的編程哲學去浸染開發者的編程思想,從而在骨子里下意識地提高代碼的優雅程度。
2.1 數據訪問
首先,作者開篇所講的是關于數據的訪問,并區分出四種訪問的方式:對字面值的訪問、對變量的訪問、對數組成員的訪問以及對對象成員的訪問。當然,訪問的快慢也是與這順序所相對應的。
- 字面值與變量的訪問比數組及對象的訪問要快
- 訪問本地變量要比訪問外部變量要快
- 盡可能不使用 with 、 try-catch 及 eval() .
- 減少對象成員的嵌套
- 訪問對象成員時,查找的原型鏈越長越消耗更多的時間
- 閉包不僅會占用內存,而且在低版本瀏覽器中導致內存泄漏,因此要權衡。
2.2 DOM 編程
第二,關于 DOM 相關的編程,我們要謹記一點:DOM 的訪問及操作猶如搭載在橋梁上的收費站,太多的訪問與操作只會帶來性能上的消耗。因此:
- 我們應該盡可能地減少對 DOM 元素的訪問,而把工作放在 JavaScript 代碼中去執行
- 若需要重復操作 DOM 元素,則把其對應的引用存儲在一個本地變量
- 像 document.getElementsByName() 、 document.getElementsByClassName() 或 document.getElementsByTagName() 等方法所放回的是一個 HTML Collections。遍歷該集合時我們需要考慮到,若該集合不怎么大時,我們可以把集合的大小緩存在一個本地變量以作循環條件中的判斷;而若集合非常大時則需要考慮把其轉換成數組來遍歷
- 若瀏覽器支持時,盡可能地使用更為高效的封裝屬性或函數,如 firstElementChild 、 document.querySelectorAll() 等。
- 盡可能地去減少頁面的重排與重繪
- 在制作動畫時,應把元素先置為絕對定位,以減少重排及重繪所帶來的消耗
- 使用事件委托機制來減少事件處理函數的數量
2.3 代碼中的算法及代碼流的控制
個人認為,為何我們寫的代碼總運行得這么慢?主要原因在于我們的算法不夠高效,而且代碼流的控制不夠優雅。所執行的代碼越多,執行的時間也就越長。因此不管是循環結構還是控制結構,作者都給出了自己的經驗之談:
- for 、 while 以及 do-while 結構都非常相似,并沒說哪種循環結構更為高效
- 除非需要遍歷一個屬性未知的對象,不然請不要輕易使用 for-in 結構
- 盡可能地減少循環的次數及每次循環的工作量,以優化循環
- 盡管 switch 結構比 if-else 結構要高效,但并非是一個最佳的選擇
- 對于代碼有多種流向的情況,Lookup 表是一種不錯的選擇
- 當算法涉及到遞歸時,請注意各瀏覽器中棧使用量的限制
- 當遞歸算法超出棧使用量的限制時,嘗試使用循環去替代算法或通過記憶的手段減少重復計算的次數
來自:https://github.com/aleen42/PersonalWiki/issues/27