Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

【摘要】本文為 Google Chrome 團隊的開發項目工程師 Addy Osmani 在PerfMatters 2019 網頁性能大會發表的“JavaScript性能優化”(https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4)的演講,其分享了處理 JavaScript 的腳本優化建議,大幅地減少了下載時間和執行時間。

視頻地址:https://youtu.be/X9eRLElSW1c(需科學上網)

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

作者 | Addy Osmani

譯者 | 蘇本如 責編 | 屠敏

出品 | CSDN(ID:CSDNnews)

以下為譯文:

在過去的幾年中,由於瀏覽器的腳本解析和編譯速度的提高,Javascript成本構成發生了巨大的變化。到了2019年,處理Javascript的開銷主要體現在腳本下載時間和CPU執行時間上。

如果瀏覽器的主線程忙於執行Javascript腳本,則用戶交互體驗可能會受影響,因此,優化腳本執行時間並消除網絡瓶頸,會對用戶體驗產生積極的作用。

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

高層級的實用指南

這對Web開發人員來說意味著什麼?意味著解析(Parse)和編譯(Compile)不再像我們曾經想象的那麼慢了。所以開發人員在優化Javascript包時,要重點關注以下三大方面:

減少下載時間

  • 確保Javascript包儘可能地小,特別是對於移動設備。較小的包可以提升下載速度、降低內存使用量,並減少CPU開銷。

  • 避免只有一個大的Javascript包;如果包大小超過50–100 KB,就將其拆分為幾個小包。(藉助HTTP/2協議的多路複用機制,多個請求和響應消息可以同時傳輸,從而減少額外請求的開銷。)

  • 對於移動設備上使用的Javascript包更要儘可能地小,一方面因為網絡帶寬的制約,另一方面需要要儘量減少內存的使用。

縮短執行時間

  • 避免持續佔用主線程並影響頁面響應時間的長時任務,現在腳本下載後的執行時間成為主要的成本開銷。

避免使用大型內聯腳本(因為它們仍然需要在主線程上進行解析和編譯)。

  • 建議參考一條經驗法則:如果一個腳本超過1KB,就不要將其內聯(因為當外部腳本大小超過1KB時,就會觸發代碼緩存)。

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

為什麼下載和執行時間很重要?

為什麼優化下載和執行時間對我們很重要?因為對於低端網絡而言,下載時間的影響非常之大。儘管4G(甚至5G)在全球範圍內增長迅速,但大多數人的有效連接速度仍然遠遠低於網絡的標稱速度。有時當我們外出時,會感覺到網速下降到只有3G的速度(甚至更糟)。

JavaScript的執行時間對於CPU較慢的低端手機也非常重要。由於CPU、GPU,和散熱限制的不同,高端和低端手機的性能差距巨大。這對JavaScript的性能影響明顯,因為它的執行受到CPU性能的制約。

事實上,在Chrome之類的瀏覽器上,JavaScript的執行時間可以達到頁面加載總耗時的30%。下圖是一個具有典型工作負載的網站(Reddit.com)在一臺高端桌面PC上的頁面加載情況分析:

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

V8引擎下的Javascript處理時間佔整個頁面加載時間的10-30%

對於移動設備,與高端手機(如Pixel 3)相比,在中端手機(如Moto G4)上執行Reddit的Javascript腳本需要3-4倍的耗時,而在低端手機(價格低於100美元的Alcatel 1X)上執行Reddit的Javascript腳本更是需要6倍以上的耗時:

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

Reddit的Javascript腳本在幾種不同設備(低端、中端和高端)上的執行時間。

注意:Reddit對於桌面和移動網絡有不同的體驗,因此MacBook Pro的執行結果無法與其他結果進行比較。

當你著手優化JavaScript的執行時間時,你需要留意可能長時間獨佔界面線程(UI Thread)的長時任務。即使頁面看起來已經加載完成,這些長時任務也會拖累關鍵任務的執行。把長時任務分解成較小的任務。通過拆分代碼並確定加載順序,你可以更快地實現頁面交互,並有望降低輸入延遲。

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

獨佔主線程的長時任務應該拆分。

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

V8引擎如何提高Javascript解析/編譯速度?

自Chrome 版本60以來,V8引擎的原始JS的解析速度增加了2倍。與此同時,Chrome還做了其他工作一些工作使得解析和編譯工作並行化,這使得這部分的成本開銷對用戶體驗的影響變得不是那麼顯著和關鍵了。

V8引擎通過將解析和編譯工作轉到worker線程上,使得主線程上的解析和編譯工作量平均減少了40%。例如,Facebook降低了46%,Pinterest降低62%,而最大的改進是是YouTube ,降低了81%。這是在現有的非主線程流解析/編譯性能改進基礎上的進一步提升。

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

不同版本的V8引擎的解析時間對比

我們還可以圖示對比不同Chrome版本的不同V8引擎對CPU處理時間的影響。可以看出,Chrome 61解析Facebook的JS腳本所花費的時間,可以供Chrome 75解析同樣的Facebook的JS腳本,和6個Twitter的JS腳本了。

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

Chrome 61解析Facebook的JS腳本所花費的時間,可以供Chrome 75解析完成同樣的Facebook的JS腳本,和6個Twitter的JS腳本了。

讓我們深入研究一下這些改進是如何實現的。總的來說,腳本資源可以在worker線程上進行流式解析和編譯,這意味著:

  • V8引擎可以在不阻塞主線程的情況下解析和編譯JavaScript。

  • 當整個HTML解析器遇到<script>標記時,就開始流式處理。遇到阻塞解析器(parse-blocking)的腳本時,HTML解析器就放棄,而對於異步腳本則繼續處理。

  • 在大多數網絡連接速度下,V8引擎的解析速度都比下載速度快,因此在最後一個腳本字節被下載後幾毫秒的時間內,V8引擎就能完成解析+編譯工作。

具體來說,很多老版本的Chrome在開始腳本解析之前,需要將腳本下載完成,這是一種簡單的方法,但它沒有充分利用CPU的能力。而從版本41到68,Chrome在下載一開始時就立即在單獨的線程上解析異步和延遲腳本。

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

JS腳本以多個塊下載。V8引擎看到大於30KB的腳本被下載後就會啟動腳本流解析工作。

Chrome 71採用了基於任務(task-based)的設置方案。調度器可以一次解析多個異步/延遲腳本,這一改進使得主線程解析時間縮短了約20%,真實網站上的TTI/FID整體提高了大約2%。

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

Chrome 71採用了基於任務(task-based)的設置,調度器可以一次解析多個異步/延遲腳本

Chrome 72開始採用流式處理作為主要的解析方式,現在常規的同步腳本(內聯腳本除外)也可以採用這種解析方式。如果主線程需要,我們也可以繼續採用基於任務的解析,從而減少不必要地重複工作。

舊版的Chrome支持流式解析和編譯,其中來自網絡的腳本源數據必須先到達Chrome主線程後,再轉發給流解析器解析。

這通常會導致這樣的情況:腳本數據已經從網絡上下載完成,但由於主線程上的其他任務(如HTML解析、排版或者JavaScript執行),阻塞了腳本數據的轉發,因此流解析器(streaming parser)不得不空等。

現在我們正嘗試在預加載時開始解析,以前主線程反彈會阻礙這種操作。

Leszek Swirski 在 BlinkOn 10 上的演講介紹了相關細節:https://youtu.be/D1UJgiG4_NI(需科學上網)

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

這些改變如何反映到DevTools中?

除上述之外,DevTools中還存在一個問題,它以表明它會獨佔 CPU(完全阻塞)的方式渲染整個解析器任務。但是,不管解析器是否需要數據(數據需要通過主線程)都會阻塞。當我我們從單個流線程轉向多個流傳輸任務時,這個問題變得非常明顯。下面是你在Chrome 69中看到的情況:

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

DevTools以表明它會獨佔CPU(完全阻塞)的方式渲染整個解析器任務

如上圖示,“解析腳本”任務需要1.08秒。但是解析JavaScript其實並沒有那麼慢!大部分時間除了等待數據通過主線程之外什麼都做不了。

而在Chrome 76中顯示的內容就不一樣了:

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

在Chrome 76中,解析工作被分解為多個較小的流任務。

一般來說,DevTools性能窗格非常適合從宏觀層面分析你的頁面。對於更具體的V8度量指標,如Javascript解析和編譯時間,我們建議使用帶有運行時調用統計(RCS)的Chrome跟蹤工具。在RCS結果中,Parse-Background和Compile-Background會告訴你在主線程外解析和編譯Javascript花費了多少時間,而Parse和Compile是針對主線程的度量指標。

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

這些改變對現實應用的影響是什麼?

讓我們來看一些真實網站的示例,來了解腳本流(script streaming)是如何工作的。

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

主線程和worker線程在MacBook Pro上解析和編譯Reddit網站的JS所花費的時間對比

Reddit.com網站有幾個超過100KB的JS包,它們包裝在外部函數中,導致在主線程上需要進行大量的延遲編譯(lazy compilation)。如上圖所示,主線程耗時才是真正關鍵的,因為主線程持續繁忙會嚴重影響交互體驗。Reddit的大部分時間花在了主線程上,而worker線程或後臺線程的使用率很低。

可以將一些較大的JS包拆分為幾個不需要包裝的小包(例如每個包50 KB),以最大限度地實現並行化,這樣每個包都可以單獨進行流解析和編譯,並在載入期間減少主線程的解析/編譯時間。

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

主線程和worker線程在MacBook Pro上解析和編譯Facebook網站的JS所花費的時間對比

我們再看看像facebook.com這樣的網站的情況。Facebook使用了大約292個請求,加載了大約6MB的壓縮JS腳本,其中一些是異步的,一些是預加載的,還有一些是低優先級的。它們的許多腳本都非常小,粒度也不大,這有助於後臺/workers線程上的整體並行化,因為這些較小的腳本可以同時進行流解析/編譯。

值得注意地是,像Facebook或Gmail這樣老牌的應用程序的桌面版本上有這麼多的腳本可能是合理的。但是你的網站可能和Facebook不一樣。不管怎樣,儘可能地簡化你的JS包,不必要的就不要裝載了。

儘管大多數JavaScript解析和編譯工作都可以在後臺線程上以流式方式進行,但仍有一些工作必須在主線程上進行。而當主線程繁忙時,頁面就無法響應用戶輸入了。所以要密切關注下載和執行代碼對用戶體驗的影響。

注意:目前並不是所有的Javascript引擎和瀏覽器都實現了腳本流(script streaming)式加載優化。但是我們仍然相信,本文的整體指導會幫助大家全面地提升用戶體驗。

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

解析JSON的開銷

JSON語法比JavaScript語法簡單很多,所以JSON的解析效率要比Javascript高得多。基於這一點,Web應用程序可以提供類似於JSON的大型配置對象文本,而不是將數據作為Javascript對象文本進行內聯,這樣可以大大提高Web應用程序的加載性能。如下所示:

const data = { foo: 42, bar: 1337 }; // 

……它可以用 JSON 字符串形式表示,然後在運行時進行 JSON 解析。如下所示:

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

只要JSON字符串只計算一次,那麼相比Javascript對象文本, JSON.parse方法就要快得多,冷加載時尤其明顯。

在為大量數據使用普通對象文本時還有一個額外的風險:它們可能會被解析兩次!

  1. 第一次是文本預解析時。

  2. 第二次是文本延遲解析時。

第一次解析是必須的,可以將對象文本放在頂層或PIFE中來避免第二次解析。

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

重複訪問時的解析/編譯情況如何?

V8引擎的(字節)代碼緩存優化可以幫助改善重複訪問時的體驗。當第一次請求腳本時,Chrome會下載腳本並將其交給V8引擎進行編譯。同時將文件存儲在瀏覽器的磁盤緩存中。當第二次請求JS文件時,Chrome會從瀏覽器緩存中獲取該文件,並再次將其交給V8引擎進行編譯。然而,這次編譯的代碼會被序列化,並作為元數據附加到緩存的腳本文件中。

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

V8引擎的代碼緩存示意圖

第三次請求腳本時,Chrome從緩存中獲取腳本文件和文件的元數據,並將兩者都交給V8引擎。V8引擎會反序列化元數據來跳過編譯步驟。如果前兩次訪問間隔小於72小時內,代碼緩存就會啟動。如果採用service worker來緩存腳本,那麼chrome也會主動啟動代碼緩存。詳細信息可以參閱 web 開發者的代碼緩存指南。

Google Chrome 工程師:JavaScript 不容錯過的八大優化建議

總結

到了2019年。腳本下載和執行的時間開銷已經變成加載腳本的主要瓶頸。所以你應該為你的首屏內容準備一個較小的同步(內聯)腳本包,其餘部分則使用一個或多個延遲腳本,並且把較大的包拆分成許多小包來按需加載。這樣一來就能充分利用 V8 引擎的並行化能力。

在移動設備上,由於網絡、內存消耗和CPU執行時間的制約,你需要儘可能地減少腳本的數量,平衡延遲和緩存設置,儘可能地讓解析和編譯工作在主線程外執行。

原文:https://v8.dev/blog/cost-of-javascript-2019

本文為 CSDN 翻譯,轉載請註明來源出處。

【End】

相關推薦

推薦中...