JavaScript啓動性能瓶頸分析與解決方案

在 Web 開發中,隨着需求的增長與代碼庫的擴張,咱們最終發佈的 Web 頁面也逐漸膨脹。不過這種膨脹遠不止意味着佔據更多的傳輸帶寬,其還意味着用戶瀏覽網頁時可能更差勁的性能體驗。瀏覽器在下載完某個頁面依賴的腳本以後,其還須要通過語法分析、解釋與運行這些步驟。而本文則會深刻分析瀏覽器對於 JavaScript 的這些處理流程,挖掘出那些影響你應用啓動時間的罪魁禍首,而且根據我我的的經驗提出相對應的解決方案。回顧過去,咱們尚未專門地考慮過如何去優化 JavaScript 解析/編譯這些步驟;咱們預想中的是解析器在發現 <script>標籤後會瞬時完成解析操做,不過這很明顯是癡人說夢。下圖是對於 V8 引擎工做原理的概述:react

下面咱們深刻其中的關鍵步驟進行分析。chrome

究竟是什麼拖慢了咱們應用的啓動時間?瀏覽器

在啓動階段,語法分析,編譯與腳本執行佔據了 JavaScript 引擎運行的絕大部分時間。換言之,這些過程形成的延遲會真實地反應到用戶可交互時延上;譬如用戶已經看到了某個按鈕,可是要好幾秒以後才能真正地去點擊操做,這一點會大大影響用戶體驗。緩存

上圖是咱們使用 Chrome Canary 內置的 V8 RunTime Call Stats 對於某個網站的分析結果;須要注意的是桌面瀏覽器中語法解析與編譯佔用的時間仍是蠻長的,而在移動端中佔用的時間則更長。實際上,對於 Facebook, Wikipedia, Reddit 這些大型網站中語法解析與編譯所佔的時間也不容忽視:安全

上圖中的粉色區域表示花費在 V8 與 Blink's C++ 中的時間,而橙色和黃色分別表示語法解析與編譯的時間佔比。Facebook 的 Sebastian Markbage 與 Google 的 Rob Wormald 也都在 Twitter 發文表示過 JavaScript 的語法解析時間過長已經成爲了避免可忽視的問題,後者還表示這也是 Angular 啓動時主要的消耗之一。網絡

隨着移動端浪潮的涌來,咱們不得不面對一個殘酷的事實:移動端對於相同包體的解析與編譯過程要花費至關於桌面瀏覽器2~5倍的時間。固然,對於高配的 iPhone 或者 Pixel 這樣的手機相較於 Moto G4 這樣的中配手機表現會好不少;這一點提醒咱們在測試的時候不能僅用身邊那些高配的手機,而應該中高低配兼顧:框架

上圖是部分桌面瀏覽器與移動端瀏覽器對於 1MB 的 JavaScript 包體進行解析的時間對比,顯而易見的能夠發現不一樣配置的移動端手機之間的巨大差別。當咱們應用包體已經很是巨大的時候,使用一些現代的打包技巧,譬如代碼分割,TreeShaking,Service Workder 緩存等等會對啓動時間有很大的影響。另外一個角度來看,即便是小模塊,你代碼寫的很糟或者使用了很糟的依賴庫都會致使你的主線程花費大量的時間在編譯或者冗餘的函數調用中。咱們必需要清醒地認識到全面評測以挖掘出真正性能瓶頸的重要性。異步

JavaScript 語法解析與編譯是否成爲了大部分網站的瓶頸?async

我曾不止一次聽到有人說,我又不是 Facebook,你說的 JavaScript 語法解析與編譯到ide

底會對其餘網站形成什麼樣的影響呢?對於這個問題我也很好奇,因而我花費了兩個月的時間對於超過 6000 個網站進行分析;這些網站囊括了 React,Angular,Ember,Vue 這些流行的框架或者庫。大部分的測試是基於 WebPageTest 進行的,所以你能夠很方便地重現這些測試結果。光纖接入的桌面瀏覽器大概須要 8 秒的時間才能容許用戶交互,而 3G 環境下的 Moto G4 大概須要 16 秒 才能容許用戶交互。

大部分應用在桌面瀏覽器中會耗費約 4 秒的時間進行 JavaScript 啓動階段(語法解析、編譯、執行):

而在移動端瀏覽器中,大概要花費額外 36% 的時間來進行語法解析:

另外,統計顯示並非全部的網站都甩給用戶一個龐大的 JS 包體,用戶下載的通過 Gzip 壓縮的平均包體大小是 410KB,這一點與 HTTPArchive 以前發佈的 420KB 的數據基本一致。不過最差勁的網站則是直接甩了 10MB 的腳本給用戶,簡直可怕。

經過上面的統計咱們能夠發現,包體體積當然重要,可是其並不是惟一因素,語法解析與編譯的耗時也不必定隨着包體體積的增加而線性增加。整體而言小的 JavaScript 包體是會加載地更快(忽略瀏覽器、設備與網絡鏈接的差別),可是一樣 200KB 的大小,不一樣開發者的包體在語法解析、編譯上的時間倒是天差地別,不可同日而語。

現代 JavaScript 語法解析 & 編譯性能評測

Chrome DevTools

打開 Timeline( Performance panel ) > Bottom-Up/Call Tree/Event Log 就會顯示出當前網站在語法解析/編譯上的時間佔比。若是你但願獲得更完整的信息,那麼能夠打開 V8 的 Runtime Call Stats。在 Canary 中,其位於 Timeline 的 Experims > V8 Runtime Call Stats 下。

Chrome Tracing

打開 about:tracing 頁面,Chrome 提供的底層的追蹤工具容許咱們使用disabled-by-default-v8.runtime_stats來深度瞭解 V8 的時間消耗狀況。V8 也提供了詳細的指南來介紹如何使用這個功能。

WebPageTest

WebPageTest 中 Processing Breakdown 頁面在咱們啓用 Chrome > Capture Dev Tools Timeline 時會自動記錄 V8 編譯、EvaluateScript 以及 FunctionCall 的時間。咱們一樣能夠經過指明disabled-by-default-v8.runtime_stats的方式來啓用 Runtime Call Stats。

更多使用說明參考個人gist。

User Timing

咱們還可使用 Nolan Lawson 推薦的User Timing API來評估語法解析的時間。不過這種方式可能會受 V8 預解析過程的影響,咱們能夠借鑑 Nolan 在 optimize-js 評測中的方式,在腳本的尾部添加隨機字符串來解決這個問題。我基於 Google Analytics 使用類似的方式來評估真實用戶與設備訪問網站時候的解析時間:

DeviceTiming

Etsy 的 DeviceTiming 工具可以模擬某些受限環境來評估頁面的語法解析與執行時間。其將本地腳本包裹在了某個儀表工具代碼內從而使咱們的頁面可以模擬從不一樣的設備中訪問。能夠閱讀 Daniel Espeset 的Benchmarking JS Parsing and Execution on Mobile Devices 一文來了解更詳細的使用方式。

咱們能夠作些什麼以下降 JavaScript 的解析時間?

減小 JavaScript 包體體積。咱們在上文中也說起,更小的包體每每意味着更少的解析工做量,也就能下降瀏覽器在解析與編譯階段的時間消耗。

使用代碼分割工具來按需傳遞代碼與懶加載剩餘模塊。這多是最佳的方式了,相似於PRPL這樣的模式鼓勵基於路由的分組,目前被 Flipkart, Housing.com 與 Twitter 普遍使用。

Script streaming: 過去 V8 鼓勵開發者使用async/defer來基於script streaming實現 10-20% 的性能提高。這個技術會容許 HTML 解析器將相應的腳本加載任務分配給專門的 script streaming 線程,從而避免阻塞文檔解析。V8 推薦儘早加載較大的模塊,畢竟咱們只有一個 streamer 線程。

評估咱們依賴的解析消耗。咱們應該儘量地選擇具備相同功能可是加載地更快的依賴,譬如使用 Preact 或者 Inferno 來代替 React,兩者相較於 React 體積更小具備更少的語法解析與編譯時間。Paul Lewis 在最近的一篇文章中也討論了框架啓動的代價,與 Sebastian Markbage 的說法不謀而合:最好地評測某個框架啓動消耗的方式就是先渲染一個界面,而後刪除,最後進行從新渲染。第一次渲染的過程會包含了分析與編譯,經過對比就能發現該框架的啓動消耗。

若是你的 JavaScript 框架支持 AOT(ahead-of-time)編譯模式,那麼可以有效地減小解析與編譯的時間。Angular 應用就受益於這種模式:

現代瀏覽器是如何提升解析與編譯速度的?

不用灰心,你並非惟一糾結於如何提高啓動時間的人,咱們 V8 團隊也一直在努力。咱們發現以前的某個評測工具 Octane 是個不錯的對於真實場景的模擬,它在微型框架與冷啓動方面很符合真實的用戶習慣。而基於這些工具,V8 團隊在過去的工做中也實現了大約 25% 的啓動性能提高:

本部分咱們就會對過去幾年中咱們使用的提高語法解析與編譯時間的技巧進行闡述。

代碼緩存

Chrome 42 開始引入了所謂的代碼緩存的概念,爲咱們提供了一種存放編譯後的代碼副本的機制,從而當用戶二次訪問該頁面時能夠避免腳本抓取、解析與編譯這些步驟。除以以外,咱們還發如今重複訪問的時候這種機制還能避免 40% 左右的編譯時間,這裏我會深刻介紹一些內容:

  •  代碼緩存會對於那些在 72 小時以內重複執行的腳本起做用。

  •  對於 Service Worker 中的腳本,代碼緩存一樣對 72 小時以內的腳本起做用。

  •  對於利用 Service Worker 緩存在 Cache Storage 中的腳本,代碼緩存能在腳本首次執行的時候起做用。

總而言之,對於主動緩存的 JavaScript 代碼,最多在第三次調用的時候其可以跳過語法分析與編譯的步驟。咱們能夠經過chrome://flags/#v8-cache-strategies-for-cache-storage來查看其中的差別,也能夠設置 js-flags=profile-deserialization運行 Chrome 來查看代碼是否加載自代碼緩存。不過須要注意的是,代碼緩存機制僅會緩存那些通過編譯的代碼,主要是指那些頂層的每每用於設置全局變量的代碼。而對於相似於函數定義這樣懶編譯的代碼並不會被緩存,不過 IIFE 一樣被包含在了 V8 中,所以這些函數也是能夠被緩存的。

Script Streaming

Script Streaming容許在後臺線程中對異步腳本執行解析操做,能夠對於頁面加載時間有大概 10% 的提高。上文也提到過,這個機制一樣會對同步腳本起做用。

這個特性卻是第一次說起,所以 V8 會容許全部的腳本,即便阻塞型的 <scriptsrc=''>腳本也能夠由後臺線程進行解析。不過缺陷就是目前僅有一個 streaming 後臺線程存在,所以咱們建議首先解析大的、關鍵性的腳本。在實踐中,咱們建議將 <scriptdefer>添加到 <head>塊內,這樣瀏覽器引擎就可以儘早地發現須要解析的腳本,而後將其分配給後臺線程進行處理。咱們也能夠查看 DevTools Timeline 來肯定腳本是否被後臺解析,特別是當你存在某個關鍵性腳本須要解析的時候,更須要肯定該腳本是由 streaming 線程解析的。

語法解析 & 編譯優化

咱們一樣致力於打造更輕量級、更快的解析器,目前 V8 主線程中最大的瓶頸在於所謂的非線性解析消耗。譬如咱們有以下的代碼片: 

(function(global, module) { … })(this, functionmodule() { my functions })

V8 並不知道咱們編譯主腳本的時候是否須要module這個模塊,所以咱們會暫時放棄編譯它。而當咱們打算編譯module時,咱們須要重分析全部的內部函數。這也就是所謂的 V8 解析時間非線性的緣由,任何一個處於 N 層深度的函數都有可能被從新分析 N 次。V8 已經可以在首次編譯的時候蒐集全部內部函數的信息,所以在將來的編譯過程當中 V8 會忽略全部的內部函數。對於上面這種module形式的函數會是很大的性能提高,建議閱讀The V8 Parser(s) — Design, Challenges, and Parsing JavaScript Better來獲取更多內容。V8 一樣在尋找合適的分流機制以保證啓動時能在後臺線程中執行 JavaScript 編譯過程。

預編譯 JavaScript?

每隔幾年就有人提出引擎應該提供一些處理預編譯腳本的機制,換言之,開發者可使用構建工具或者其餘服務端工具將腳本轉化爲字節碼,而後瀏覽器直接運行這些字節碼便可。從我我的觀點來看,直接傳送字節碼意味着更大的包體,勢必會增長加載時間;而且咱們須要去對代碼進行簽名以保證可以安全運行。目前咱們對於 V8 的定位是儘量地避免上文所說的內部重分析以提升啓動時間,而預編譯則會帶來額外的風險。不過咱們歡迎你們一塊兒來討論這個問題,雖然 V8 目前專一於提高編譯效率以及推廣利用 Service Worker 緩存腳本代碼來提高啓動效率。咱們在 BlinkOn7 上與 Facebook 以及 Akamai 也討論過預編譯相關內容。

Optimize JS 優化

相似於 V8 這樣的 JavaScript 引擎在進行完整的解析以前會對腳本中的大部分函數進行預解析,這主要是考慮到大部分頁面中包含的 JavaScript 函數並不會馬上被執行。

預編譯可以經過只處理那些瀏覽器運行所須要的最小函數集合來提高啓動時間,不過這種機制在 IIFE 面前卻反而下降了效率。儘管引擎但願避免對這些函數進行預處理,可是遠不如optimize-js這樣的庫有做用。optimize-js 會在引擎以前對於腳本進行處理,對於那些當即執行的函數插入圓括號從而保證更快速地執行。這種預處理對於 Browserify, Webpack 生成包體這樣包含了大量即刻執行的小模塊起到了很是不錯的優化效果。儘管這種小技巧並不是 V8 所但願使用的,可是在當前階段不得不引入相應的優化機制。

總結

啓動階段的性能相當重要,緩慢的解析、編譯與執行時間可能成爲你網頁性能的瓶頸所在。咱們應該評估頁面在這個階段的時間佔比而且選擇合適的方式來優化。咱們也會繼續致力於提高 V8 的啓動性能,盡我所能!

【責任編輯:龐桂玉 TEL:(010)68476606】

相關文章
相關標籤/搜索