原文連結:https://medium.com/dev-channel/the-cost-of-javascript-84009f51e99e
當我們建立的網站越來越多依賴Javascript時,我們有時候會為了一些我們不容易看到的地方付出代價。在這邊文章中,我會講述一些規範,如果你想讓你的網站在移動裝置上更快載入和響應的話,這些規範可能會對你有幫助。
tl;dr: less code = less parse/compile + less transfer + less to decompress
tl;dr是too long; don't read的縮寫
網路
當大多數開發者想到Javascript的開銷時,可能會聯想到下載和執行開銷。傳送的資料量越大,那使用者連線的速度就越緩慢。
即使在一些大國家,如果使用者的有效網路不是3G, 4G 或者WiFi的話(使用者可能在咖啡廳用著wifi,但是實際網速是2G),那麼這就會成為一個問題。
你可以通過以下幾種方式來減少網路傳輸的開銷:
- 只傳輸使用者需要的. 程式碼分離(Code-spliting)能起到一定作用.
- 混淆(醜化) (ES5有Uglify, ES2015有babel-minify 或者 uglify-es)
- 強力壓縮 (使用 Brotli ~q11, Zopfli 或者 gzip). 相較於gzip,Brotli 在壓縮比上更勝一籌. 它使CertSimple在JS的體積上節省了17%,使LinkedIn在載入時間上節省了4%.
- 刪除無用程式碼. 與DevTools code coverage類似. 對於剝離的程式碼, 見 tree-shaking, Closure Compiler的高階優化和其它類似的外掛,像 lodash-babel-plugin 或者 Webpack的 ContextReplacementPlugin. 使用babel-preset-env 和 browserlist 以避免轉換已經存在於現代瀏覽器的一些特性. 資深開發者可能已經發現 analysis of their Webpack bundles 可以幫助去除那些不必要的依賴.
- 快取 優化指令碼有效時間以及ETag來避免傳輸沒有變化的資料. Service Worker快取能幫助實現彈性網路,並且使你更早使用一些特性,像V8’s code cache. 同時也可以通過filename hashing學習持久化快取.
解析/編譯
一旦指令碼下載完成了之後,JS最大的開銷之一就是JS引擎的解析/編譯程式碼。在Chrome開發者工具裡的“效能”模組,解析和編譯程式碼用黃色標識.
通過Bottom-Up/Call Tree(呼叫樹),可以看實際的解析/編譯用時:
但是,為什麼這個很重要呢?
花費大量時間在解析/編譯程式碼上會延遲使用者與網站的互動,破壞使用者體驗。在網站呈現之前,JS量越大,花在解析/編譯上的時間就越長。
對於大小相同的JS和圖片或者網頁字型,JS需要瀏覽器花費的時間最多— Tom Dale
與Javascript相比,處理相同大小的圖片所需要的開銷明顯小很多。
當我們討論解析和編譯的速度之慢時,上下文是很重要的。我們的討論是基於平均水平的移動裝置。平均水平的使用者使用的移動裝置可能是CPU/GPU很慢的、沒有L2/L3快取的或者甚至記憶體很有限的。
網路和裝置不總是匹配的。一個使用者可能有很好的網路條件,但是隻有一部爛手機。相反,一個使用者可能有一部神機,卻碰上了龜速網路 — Kristofer Baxter, LinkedIn
在JavaScript Start-up Performance文中, 我提到了分別在低端和高階機型中1MB原始JS程式碼的解析時間. 它們之間的差距達到了2-5倍.
那實際的網站如何呢,比如CNN.com?
在高階機iPhone 8上,解析/編譯JS程式碼只需大約4s,而在平均水平的手機Moto G4上卻要花上將近13s。這很明顯得影響了使用者能夠多快看到介面。
這就要求我們要更加註重一些平均水平的裝置的測試,而不僅僅是自己口袋裡的高階機。上下文是很重要的:一定要針對你的使用者的裝置和網路進行優化。
可以通過 mobile device classes 來看看真實使用者的分析情況。
我們真的是傳輸了太多的JS程式碼嗎? 呃。。有可能 :)
使用HTTP Archive (top ~500K sites)來分析JS在移動裝置上的使用情況JavaScript on mobile, 我們就能發現50%左右的網站需要14s以上才能真正讓使用者用上。這些網站光花在解析/編譯JS上的時間就多達4s.
介於以上這些情況,怪不得使用者在還沒有看到頁面之前就離開了。我們當然可以在這點上做得更好。
刪除一些非必要JS程式碼能有效減少轉換時間、CPU的解析/編譯時間以及記憶體佔用。同樣也能是使用者更快得與網站互動。
執行時間
當然,編譯和解析只是JS開銷的一部分。執行JS程式碼也是主執行緒上必須要做的,如果執行時間冗長,也會直接影響使用者體驗。
一旦指令碼執行時間超過50ms,後果不堪設想 — Alex Russell
為了減少執行時間,你可以將JS程式碼分離成一塊塊的,以免阻塞主執行緒。
設計模式
有時候一些設計模式能夠幫助你,比如基於路由的程式碼分塊 (route-based chunking) 或者PRPL.
如下圖所示,PRPL就是一個利用程式碼分離和快取方式來優化互動體驗的模式:
讓我們來看看這個影響.
我們使用V8的 Runtime Call Stats 分析了一些主流網站以及PWA的載入時間。可以看到,解析時間在整個載入時間中佔了可觀的部分:
Wego,是使用了PRPL的一個站點,讓每個路由都保持很少的解析時間,使得使用者能更快得與網站互動。上圖中的很多網站也是採用了程式碼分離和效能預算 (performance budget) 來嘗試降低JS開銷。
其它開銷
JS也會在其它方面影響頁面效能:
-
記憶體。頁面可能會因為垃圾回收而導致頻繁的閃爍或暫停。當瀏覽器回收記憶體的時候,JS執行就會暫停。這就導致了當瀏覽器頻繁回收垃圾時,JS執行的暫停頻率就會比我們想象中的更多。避免記憶體洩漏和頻繁的垃圾回收能夠是頁面更穩定。
-
在執行時,如果JS執行時間過長就會阻塞主程式,導致頁面無法互動。把這些任務分成一小塊一小塊 (可以採用 requestAnimationFrame() 或者 requestIdleCallback() 或者 scheduling) 能夠最小化其帶來的影響。
漸進式 Bootstrapping
為了讓網站更快呈現在使用者面前,許多網站會使用伺服器端渲染來實現,然後在頁面返回後通過繫結事件來“升級”它。
當心 -- 這種方式也有它的開銷。一方面傳輸回來的HTML比較大,另一方面使用者必須等到JS處理完畢才能真正與頁面進行互動。
漸進式 Bootstrapping 可能是一種更好的方法。先傳送一部分功能性頁面(只是當前路由需要的HTML/JS/CSS)回來。當更多的資源傳輸回來時,頁面就會進行懶載入並且解鎖更多的功能。
載入當前頁面的程式碼實在是非常好的方法。PRPL和漸進式 Bootstrapping 就是能幫助實現這點的模式。
結論
在網路不佳的情況下,傳輸資料的大小是至關重要的。對於CPU不給力的裝置,解析時間是很重要的。
參考 Alex Russell 的 “Can You Afford It?: Real-world Web Performance Budgets”。
如果你正在搭建一個基於移動裝置的站點,儘量在典型的裝置上開發。減少JS解析/編譯時間,採用Performance Budget讓團隊成員都能檢測JS開銷。
硬廣
這是本人的前端技術小程式,基本上所有的文章都會同步更新在小程式中。歡迎大家來湊熱鬧。