[譯] 2019 年的 JavaScript 性能

本文翻譯自 V8 官方博客的這篇《The Cost Of JavaScript in 2019》,原文做者:Addy Osmani (@addyosmani[1])。 建議閱讀本文前先讀完這篇文章:使用Script-Streaming提高頁面加載性能javascript

首發於貓眼前端團隊公衆號:MY-FEE前端

過去幾年中,JavaScript 性能的大幅改進很大程度上依賴於瀏覽器解析和編譯 JavaScript 的速度。在2019 年,處理 JavaScript 的主要性能損耗在於下載和 CPU 執行時間。java

瀏覽器主線程忙於執行 JavaScript 時,用戶交互會被延遲,所以腳本執行時間和網絡上的瓶頸優化尤爲重要。git

可行的高級指南


這對於 web 開發者意味着什麼?解析和編譯的性能損耗再也不像從前咱們認爲的那樣慢。咱們須要關注三點:github

  • 提高下載速度
    • 減少 JavaScript 包的體積,尤爲是在移動設備上。更小的包能夠提高下載速度,帶來更低的內存佔用,並減小 CPU 性能損耗。
    • 避免把代碼打包成一個大文件。若是一個包超過 50–100 kB,把它分割成多個更小的包。(因爲 HTTP/2 的多路複用特性,多個請求和響應能夠同時到達,從而減小額外請求的負載。)
    • 因爲移動設備上的網絡速度,你應該減小網絡傳輸,並且也須要維持更低的內存使用。
  • 提高執行速度
    • 避免使主線程忙碌的長任務,使頁面快點進行可交互態。腳本執行時間目前成爲了一個主要的性能損耗。
  • 避免大型內聯腳本(由於它們也會在主線程中解析和編譯)。一個不錯的規定是:若是腳本超過 1KB,就不要將其內聯(外部腳本的字節碼緩存要求最小爲 1KB 也是一個緣由).

爲什麼優化下載和執行時間很重要?


爲什麼優化下載和執行時間很重要?下載時間在低端網絡環境下很關鍵。儘管 4G(甚至 5G)在全球範圍快速發展,咱們實際感覺到的網絡速度和宣傳並不一致,不少時候感受就像 3G(甚至更差)。web

JavaScript 執行時間在使用低端 CPU 的手機上很重要。因爲 CPU、GPU 和散熱上的差別,不一樣手機上性能差別很是大。這會影響到 JavaScript 的性能,由於 JavaScript 的執行是 CPU 密集型任務。瀏覽器

實際上,像 Chrome 這樣的瀏覽器上的頁面加載總時間,有多達 30% 的時間花在 JavaScript 執行上。下面是一個任務負載(Reddit.com)很典型的網站在高端桌面設備上的頁面加載,緩存

V8 中的 JavaScript 處理佔用了頁面加載時間的 10-30%。bash

移動設備上,中端機(Moto G4)的 JavaScript 執行時間是高端機(Pixel 3)的 3 到 4 倍,低端機(不到100 刀的 Alcatel 1X)上有超過 6 倍的性能差別:網絡

Reddit 在不一樣設備類型上(低端、中端和高端)的 JavaScript 性能損耗

注意: Reddit 在桌面端和移動端的體驗徹底不一樣,所以 MacBook Pro 上的結果並不能和其餘設備上的結果直接作比較。

當你嘗試優化 JavaScript 執行時間,注意關注長任務,它可能長期獨佔 UI 線程。這些任務會阻塞執行關鍵任務,即使頁面看起來已經加載完成。把長任務拆分紅多個小任務。經過代碼分割和指定加載優先級,能夠提高頁面可交互速度,而且有但願下降輸入延遲。

長任務獨佔主線程,應該拆分它們。

V8 在提高解析編譯速度上作了什麼?


Chrome 60+ 上,V8 對於初始 JavaScript 的解析速度提高了 2 倍。與此同時, 因爲 Chrome 上的其餘並行優化,初始解析和編譯的性能損耗更少了。

V8 減小了主線程上的解析編譯任務,平均減小了 40%(好比 Facebook 上是 46%,Pinterest 上是 62%),最高減小了 81%(YouTube),這得益於將解析編譯任務搬到了 worker 線程上。這對於流式解析/編譯是一個補充。

不一樣 V8 版本上的解析時間

下圖形象呈現了不一樣 Chrome V8 版本上 CPU 解析時間。Chrome 61 解析 Facebook 的 JS 花了相同的時間,Chrome 75 如今解析 Facebook 的時間是 Twitter 的 6 倍。

Chrome 61 解析 Facebook 的 JS 時間,Chrome 75 能夠同時解析 Facebook 和 6次 Twitter 的 JS。

咱們來研究下這些釋放出來的改變。長話短說,流式解析和 worker 線程編譯腳本,這意味着:

  • V8 能夠解析編譯 JavaScript 時不阻塞主線程。
  • 流式解析始於整個 HTML 解析器遇到 <script> 標籤。對於阻塞解析的腳本,HTML 解析器會暫停,而異步腳本會繼續執行。
  • 對於大多數真實世界的網絡鏈接速度,V8 解析比下載快,所以 V8 在腳本下載完後很快就完成了解析編譯。

稍微解釋下...很老的 Chrome 上會在完整下載完腳本後纔開始解析,這很直接但並無徹底利用好 CPU。Chrome 41 和 68 之間的版本上,Chrome 在下載一開始就在一個獨立線程上解析 async 和 defer 的腳本。

頁面上的腳本被分割成多個塊。只要代碼塊超過 30KB,V8 就會開始流式解析。

Chrome 71 上,咱們開始作一個基於任務的調整,調度器能夠一次解析多個 async/defer 腳本。這一改變的影響是,主線程解析時間減小 20%,在真實網站上,帶來超過 2% 的 TTI/FID 提高。

譯者注:FID(First Input Delay),第一輸入延遲(FID)測量用戶首次與您的站點交互時的時間(即,當他們單擊連接,點擊按鈕或使用自定義的JavaScript驅動控件時)到瀏覽器實際可以的時間迴應這種互動。交互時間(TTI)是衡量應用加載所需時間並可以快速響應用戶交互的指標。

Chrome 72 上,咱們轉向使用流式解析做爲主要解析方式:如今通常異步的腳本都以這種方式解析(內聯腳本除外)。咱們也中止了廢除基於任務的解析,若是主線程須要的話,由於那樣只是在作沒必要要的重複工做。

早期版本的 Chrome 支持流式解析和編譯,來自網絡的腳本源數據必須先到達 Chrome 的主線程,而後纔會轉發給流處理器。

這常會形成流式解析器等待早已下載完成但尚未被轉發到流任務的數據,由於它被主線程上的其餘任務(好比 HTML 解析,佈局或者 JavaScript 執行)所阻塞。

咱們如今正在嘗試開始對預加載進行解析,而主線程彈跳會事先對此造成阻塞。

Leszek Swirski 的 BlinkOn 演示呈現了更多細節:

DevTools 上如何查看這些改變?


除了上述以外,DevTools 有個問題, 它暗中使用了 CPU,這會影響到整個解析任務的呈現。然而,解析器解析數據時就會阻塞(它須要在主線程上運行)。自從咱們從一個單一的流處理線程中移動到流任務中,這一點就變成更爲明顯了。下面是你在 Chrome 69 中常常會看到的:

上圖中的「解析腳本」任務花了 1.08 秒。而解析 JavaScript 其實並不慢!多數時間裏除了等待數據經過主線程以外什麼都不作。

Chrome 76 的表現大不相同:

Chrome 76 上,解析腳本被拆分紅多個更小的流式任務。

一般,DevTools 性能面板很適合用來查看頁面上發生的行爲。對於更詳細的 V8 特定指標,好比 JavaScript 解析編譯時間,咱們推薦使用帶有運行時調用統計(RCS)的 Chrome Tracing。RCS 結果中,Parse-BackgroundCompile-Background 表明主線程以外解析和編譯 JavaScript 花費的時間,然而 ParseCompile 記錄了主線程的指標。

這些改變的真實影響?


來看一些真實網站的例子和腳本流式解析如何應用。

在 MacBook Pro 上,主線程和 workder 線程解析編譯 Reddit 的 JS 所花的時間。

Reddit.com 有多個 100 KB+ 的代碼包,這些包被包裝在引發主線程大量懶編譯的外部函數中。在上圖中,因爲主線程忙碌會延遲可交互時間,其運行時間相當重要。Reddit 花了多數時間在主線程上,Work/Background 線程的利用率很低。

這得益於將大包分割成多個小包(好比每一個 50KB),以達到最大並行化,從而每一個包均可以被獨立地流式解析編譯,減輕主線程在啓動階段的壓力。

Facebook 在 Macbook Pro 上的主線程和 worker 線程解析編譯時間對比

再來看看 Facebook.com。Facebook經過 292 個請求加載了 6MB 壓縮後的 JS,其中有些是異步的,有些是預加載的,還有些的加載優先級較低。它們不少 JavaScript 的粒度都很是小 - 這對 Background/Worker 線程上的總體並行化頗有用,由於這些小的 JavaScript 能夠同時被流式解析編譯。

注意,你可能不是 Facebook,極可能沒有一個相似 Facebook 或者 Gmail 這樣的長壽應用,在桌面端,它們放如此多的 JavaScript 是無可非議的。然而,通常來講,應該讓你的包的粒度較粗,而且按需加載。

儘管多數 JavaScript 解析編譯任務能夠在 background 線程中以流的形式完成,可是某些任務仍然必需要在主線程中進行。當主線程忙碌時,頁面不能響應用戶輸入。注意關注下載執行代碼對你的用戶體驗形成的影響。

注意: 當下,不是全部的 JavaScript 引擎和瀏覽器都實現了 script streaming 來優化加載。但咱們相信你們爲了優秀用戶體驗會加入這項優化的。

解析 JSON 的性能損耗


因爲 JSON 語法比 JavaScript 語法簡單得多,解析 JSON 也會更快。這一點能夠用於提高 web 應用的啓動性能,咱們可使用相似 JSON 的對象字面量配置(好比內聯 Redux store)。不要使用 JavaScript 對象字面量來內聯數據,好比這樣:

const data = { foo: 42, bar: 1337 }; // 🐌
複製代碼

它能夠被表示成字符串化的 JSON 格式,運行時會變成解析後的 JSON:

const data = JSON.parse('{"foo":42,"bar":1337}'); // 🚀
複製代碼

若 JSON 字符串只被執行一次,尤爲是在冷啓動階段,JSON.parse 方法相比 JavaScript 對象字面量會快得多。在大於 10 KB 的對象上使用這個技巧的效果更佳 - 但在實際應用前,仍是先要測試下真實效果。

在大型數據上使用普通對象字面量還有個風險:它們可能被解析兩次!

  1. 第一次發生於字面量預解析階段。
  2. 第二次發生於字面量懶解析階段。

第一次解析沒法避免。幸運地,第二次能夠經過將對象字面量放在頂層來避免,或者放在 PIFE

關於重複訪問上的解析/編譯?


V8 的字節碼緩存優化大有幫助。當首次請求 JavaScript,Chrome 下載而後將其交給 V8 編譯。Chrome 也會將文件存進瀏覽器的磁盤緩存中。當 JS 文件再次請求,Chrome 從瀏覽器緩存中將其取出,並再次將其交給 V8 編譯。這個時候,編譯後代碼是序列化後的,會做爲元數據被添加到緩存的腳本文件上。

V8 中的字節碼緩存工做示意圖

第三次,Chrome 將文件和文件元數據從緩存中取出,一塊兒交給 V8 處理。V8 對元數據做反序列化,這樣能夠跳過編譯。字節碼緩存會在 72 小時內的前兩次訪問生效。配合使用 service worker 來緩存 JavaScript 代碼,Chrome 的字節碼緩存效果更佳。你能夠在給開發者講的字節碼緩存這篇文章中瞭解到更多細節。

結論


2019 年,下載和執行時間是加載 JavaScript 的主要瓶頸。首屏展現內容裏使用異步的(內聯)JavaScript的小型包,頁面剩下部分使用延遲(deferred)加載的 JavaScript。分解大型包,實現代碼按需加載。這樣能夠最大化 V8 中的並行解析。

移動設備上,考慮到網絡、內存使用和低端 CPU 上的執行時間,你應該傳輸更少的 JavaScript。平衡可緩存性和延遲,實如今主線程以外解析編譯任務數量的最大化。

進一步閱讀

相關文章
相關標籤/搜索