摘要】本文為 Google Chrome 團隊的開發項目工程師 Addy Osmani 在PerfMatters 2019 網頁性能大會發表的“JavaScript性能優化”(https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4)的演講,其分享了處理 JavaScript 的腳本優化建議,大幅地減少了下載時間和執行時間。
視頻地址:https://youtu.be/X9eRLElSW1c(需科學上網)
作者 | Addy Osmani
譯者 | 蘇本如 責編 | 屠敏
出品 | CSDN(ID:CSDNnews)
以下為譯文:
在過去的幾年中,由于瀏覽器的腳本解析和編譯速度的提高,Javascript成本構成發生了巨大的變化。到了2019年,處理Javascript的開銷主要體現在腳本下載時間和CPU執行時間上。
如果瀏覽器的主線程忙于執行Javascript腳本,則用戶交互體驗可能會受影響,因此,優化腳本執行時間并消除網絡瓶頸,會對用戶體驗產生積極的作用。
高層級的實用指南
這對Web開發人員來說意味著什么?意味著解析(Parse)和編譯(Compile)不再像我們曾經想象的那么慢了。所以開發人員在優化Javascript包時,要重點關注以下三大方面:
減少下載時間
確保Javascript包盡可能地小,特別是對于移動設備。較小的包可以提升下載速度、降低內存使用量,并減少CPU開銷。
避免只有一個大的Javascript包;如果包大小超過50–100 KB,就將其拆分為幾個小包。(借助HTTP/2協議的多路復用機制,多個請求和響應消息可以同時傳輸,從而減少額外請求的開銷。)
對于移動設備上使用的Javascript包更要盡可能地小,一方面因為網絡帶寬的制約,另一方面需要要盡量減少內存的使用。
縮短執行時間
避免持續占用主線程并影響頁面響應時間的長時任務,現在腳本下載后的執行時間成為主要的成本開銷。
避免使用大型內聯腳本(因為它們仍然需要在主線程上進行解析和編譯)。
建議參考一條經驗法則:如果一個腳本超過1KB,就不要將其內聯(因為當外部腳本大小超過1KB時,就會觸發代碼緩存)。
為什么下載和執行時間很重要?
為什么優化下載和執行時間對我們很重要?因為對于低端網絡而言,下載時間的影響非常之大。盡管4G(甚至5G)在全球范圍內增長迅速,但大多數人的有效連接速度仍然遠遠低于網絡的標稱速度。有時當我們外出時,會感覺到網速下降到只有3G的速度(甚至更糟)。
JavaScript的執行時間對于CPU較慢的低端手機也非常重要。由于CPU、GPU,和散熱限制的不同,高端和低端手機的性能差距巨大。這對JavaScript的性能影響明顯,因為它的執行受到CPU性能的制約。
事實上,在Chrome之類的瀏覽器上,JavaScript的執行時間可以達到頁面加載總耗時的30%。下圖是一個具有典型工作負載的網站(Reddit.com)在一臺高端桌面PC上的頁面加載情況分析:
V8引擎下的Javascript處理時間占整個頁面加載時間的10-30%
對于移動設備,與高端手機(如Pixel 3)相比,在中端手機(如Moto G4)上執行Reddit的Javascript腳本需要3-4倍的耗時,而在低端手機(價格低于100美元的Alcatel 1X)上執行Reddit的Javascript腳本更是需要6倍以上的耗時:
Reddit的Javascript腳本在幾種不同設備(低端、中端和高端)上的執行時間。
注意:Reddit對于桌面和移動網絡有不同的體驗,因此MacBook Pro的執行結果無法與其他結果進行比較。
當你著手優化JavaScript的執行時間時,你需要留意可能長時間獨占界面線程(UI Thread)的長時任務。即使頁面看起來已經加載完成,這些長時任務也會拖累關鍵任務的執行。把長時任務分解成較小的任務。通過拆分代碼并確定加載順序,你可以更快地實現頁面交互,并有望降低輸入延遲。
獨占主線程的長時任務應該拆分。
V8引擎如何提高Javascript解析/編譯速度?
自Chrome 版本60以來,V8引擎的原始JS的解析速度增加了2倍。與此同時,Chrome還做了其他工作一些工作使得解析和編譯工作并行化,這使得這部分的成本開銷對用戶體驗的影響變得不是那么顯著和關鍵了。
V8引擎通過將解析和編譯工作轉到worker線程上,使得主線程上的解析和編譯工作量平均減少了40%。例如,Facebook降低了46%,Pinterest降低62%,而最大的改進是是YouTube ,降低了81%。這是在現有的非主線程流解析/編譯性能改進基礎上的進一步提升。
不同版本的V8引擎的解析時間對比
我們還可以圖示對比不同Chrome版本的不同V8引擎對CPU處理時間的影響。可以看出,Chrome 61解析Facebook的JS腳本所花費的時間,可以供Chrome 75解析同樣的Facebook的JS腳本,和6個Twitter的JS腳本了。
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在下載一開始時就立即在單獨的線程上解析異步和延遲腳本。
JS腳本以多個塊下載。V8引擎看到大于30KB的腳本被下載后就會啟動腳本流解析工作。
Chrome 71采用了基于任務(task-based)的設置方案。調度器可以一次解析多個異步/延遲腳本,這一改進使得主線程解析時間縮短了約20%,真實網站上的TTI/FID整體提高了大約2%。
Chrome 71采用了基于任務(task-based)的設置,調度器可以一次解析多個異步/延遲腳本
Chrome 72開始采用流式處理作為主要的解析方式,現在常規的同步腳本(內聯腳本除外)也可以采用這種解析方式。如果主線程需要,我們也可以繼續采用基于任務的解析,從而減少不必要地重復工作。
舊版的Chrome支持流式解析和編譯,其中來自網絡的腳本源數據必須先到達Chrome主線程后,再轉發給流解析器解析。
這通常會導致這樣的情況:腳本數據已經從網絡上下載完成,但由于主線程上的其他任務(如HTML解析、排版或者JavaScript執行),阻塞了腳本數據的轉發,因此流解析器(streaming parser)不得不空等。
現在我們正嘗試在預加載時開始解析,以前主線程反彈會阻礙這種操作。
Leszek Swirski 在 BlinkOn 10 上的演講介紹了相關細節:https://youtu.be/D1UJgiG4_NI(需科學上網)
這些改變如何反映到DevTools中?
除上述之外,DevTools中還存在一個問題,它以表明它會獨占 CPU(完全阻塞)的方式渲染整個解析器任務。但是,不管解析器是否需要數據(數據需要通過主線程)都會阻塞。當我我們從單個流線程轉向多個流傳輸任務時,這個問題變得非常明顯。下面是你在Chrome 69中看到的情況:
DevTools以表明它會獨占CPU(完全阻塞)的方式渲染整個解析器任務
如上圖示,“解析腳本”任務需要1.08秒。但是解析JavaScript其實并沒有那么慢!大部分時間除了等待數據通過主線程之外什么都做不了。
而在Chrome 76中顯示的內容就不一樣了:
在Chrome 76中,解析工作被分解為多個較小的流任務。
一般來說,DevTools性能窗格非常適合從宏觀層面分析你的頁面。對于更具體的V8度量指標,如Javascript解析和編譯時間,我們建議使用帶有運行時調用統計(RCS)的Chrome跟蹤工具。在RCS結果中,Parse-Background和Compile-Background會告訴你在主線程外解析和編譯Javascript花費了多少時間,而Parse和Compile是針對主線程的度量指標。
這些改變對現實應用的影響是什么?
讓我們來看一些真實網站的示例,來了解腳本流(script streaming)是如何工作的。
主線程和worker線程在MacBook Pro上解析和編譯Reddit網站的JS所花費的時間對比
Reddit.com網站有幾個超過100KB的JS包,它們包裝在外部函數中,導致在主線程上需要進行大量的延遲編譯(lazy compilation)。如上圖所示,主線程耗時才是真正關鍵的,因為主線程持續繁忙會嚴重影響交互體驗。Reddit的大部分時間花在了主線程上,而worker線程或后臺線程的使用率很低。
可以將一些較大的JS包拆分為幾個不需要包裝的小包(例如每個包50 KB),以最大限度地實現并行化,這樣每個包都可以單獨進行流解析和編譯,并在載入期間減少主線程的解析/編譯時間。
主線程和worker線程在MacBook Pro上解析和編譯Facebook網站的JS所花費的時間對比
我們再看看像facebook.com這樣的網站的情況。Facebook使用了大約292個請求,加載了大約6MB的壓縮JS腳本,其中一些是異步的,一些是預加載的,還有一些是低優先級的。它們的許多腳本都非常小,粒度也不大,這有助于后臺/workers線程上的整體并行化,因為這些較小的腳本可以同時進行流解析/編譯。
值得注意地是,像Facebook或Gmail這樣老牌的應用程序的桌面版本上有這么多的腳本可能是合理的。但是你的網站可能和Facebook不一樣。不管怎樣,盡可能地簡化你的JS包,不必要的就不要裝載了。
盡管大多數JavaScript解析和編譯工作都可以在后臺線程上以流式方式進行,但仍有一些工作必須在主線程上進行。而當主線程繁忙時,頁面就無法響應用戶輸入了。所以要密切關注下載和執行代碼對用戶體驗的影響。
注意:目前并不是所有的Javascript引擎和瀏覽器都實現了腳本流(script streaming)式加載優化。但是我們仍然相信,本文的整體指導會幫助大家全面地提升用戶體驗。
解析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方法就要快得多,冷加載時尤其明顯。
在為大量數據使用普通對象文本時還有一個額外的風險:它們可能會被解析兩次!
第一次是文本預解析時。
第二次是文本延遲解析時。
第一次解析是必須的,可以將對象文本放在頂層或PIFE中來避免第二次解析。
重復訪問時的解析/編譯情況如何?
V8引擎的(字節)代碼緩存優化可以幫助改善重復訪問時的體驗。當第一次請求腳本時,Chrome會下載腳本并將其交給V8引擎進行編譯。同時將文件存儲在瀏覽器的磁盤緩存中。當第二次請求JS文件時,Chrome會從瀏覽器緩存中獲取該文件,并再次將其交給V8引擎進行編譯。然而,這次編譯的代碼會被序列化,并作為元數據附加到緩存的腳本文件中。
V8引擎的代碼緩存示意圖
第三次請求腳本時,Chrome從緩存中獲取腳本文件和文件的元數據,并將兩者都交給V8引擎。V8引擎會反序列化元數據來跳過編譯步驟。如果前兩次訪問間隔小于72小時內,代碼緩存就會啟動。如果采用service worker來緩存腳本,那么chrome也會主動啟動代碼緩存。詳細信息可以參閱 web 開發者的代碼緩存指南。
總結
到了2019年。腳本下載和執行的時間開銷已經變成加載腳本的主要瓶頸。所以你應該為你的首屏內容準備一個較小的同步(內聯)腳本包,其余部分則使用一個或多個延遲腳本,并且把較大的包拆分成許多小包來按需加載。這樣一來就能充分利用 V8 引擎的并行化能力。
在移動設備上,由于網絡、內存消耗和CPU執行時間的制約,你需要盡可能地減少腳本的數量,平衡延遲和緩存設置,盡可能地讓解析和編譯工作在主線程外執行。
原文:https://v8.dev/blog/cost-of-javascript-2019
本文為 CSDN 翻譯,轉載請注明來源出處。
【End】
白何謂Margin Collapse
不同于其他很多屬性,盒模型中垂直方向上的Margin會在相遇時發生崩塌,也就是說當某個元素的底部Margin與另一個元素的頂部Margin相鄰時,只有二者中的較大值會被保留下來,可以從下面這個簡單的例子來學習:
.square { width: 80px; height: 80px; }.red { background-color: #F44336; margin-bottom: 40px; }.blue { background-color: #2196F3; margin-top: 30px; }
在上述例子中我們會發現,紅色和藍色方塊的外邊距并沒有相加得到70px,而是只有紅色的下外邊距保留了下來。我們可以使用一些方法來避免這種行為,不過建議來說還是盡量統一使用margin-bottom
屬性,這樣就顯得和諧多了。
使用Flexbox進行布局
在傳統的布局中我們習慣使用Floats或者inline-blocks,不過它們更適合于格式化文檔,而不是整個網站。而Flexbox則是專門的用于進行布局的工具。Flexbox模型允許開發者使用很多便捷可擴展的屬性來進行布局,估計你一旦用上就舍不得了:
.container { display: flex; /* Don't forget to add prefixes for Safari */display: -webkit-flex; }
我們已經在Tutorialzine上提供了很多的關于Flexbox的介紹與小技巧,譬如5 Flexbox Techniques You Need to Know About。
使用CSS Reset
雖然這些年來隨著瀏覽器的迅速發展與規范的統一,瀏覽器特性碎片化的情況有所改善,但是在不同的瀏覽器之間仍然存在著很多的行為差異。而解決這種問題的最好的辦法就是使用某個CSS Reset來為所有的元素設置統一的樣式,保證你能在相對統一干凈的樣式表的基礎上開始工作。目前流行的Reset庫有 normalize.css, minireset以及 ress ,它們都可以修正很多已知的瀏覽器之間的差異性。而如果你不打算用某個外在的庫,那么建議可以使用如下的基本規則:
* { margin: 0; padding: 0; box-sizing: border-box; }
上面的規則看起來沒啥用,不過如果不同的瀏覽器在默認情況下為你設置了不同的外邊距/內邊距的默認值,還是會挺麻煩的。
一切應為Border-box
雖然很多初學者并不了解box-sizing
這個屬性,但是它確實相當的重要。而最好的理解它的方式就是看看它的兩種取值:
默認值為content-box,即當我們設置某個元素的heght/width屬性時,僅僅會作用于其內容尺寸。而所有的內邊距與邊都是在其之上的累加,譬如某個<div>
標簽設置為寬100,內邊距為10,那么最終元素會占用120(100 + 2*10)的像素。
border-box:內邊距與邊是包含在了width/height之內,譬如設置了width:100px
的<div>
無論其內邊距或者邊長設置為多少,其占有的大小都是100px。
將元素設置為border-box會很方便你進行樣式布局,這樣的話你就可以在父元素設置高寬限制而不擔心子元素的內邊距或者邊打破了這種限制。
以背景圖方式使用Images
如果需要在響應式的環境下展示圖片,有個簡單的小技巧就是使用該圖片作為某個<div>
的背景圖而不是直接使用img標簽。基于這種方式配合上background-size
與background-position
這兩個屬性,可以很方便地按比例縮放:
img { width: 300px; height: 200px; }div { width: 300px; height: 200px; background: url('http://cdn.tutorialzine.com/wp-content/uploads/2016/08/bicycle.jpg'); background-position: center center; background-size: cover; }section{ float: left; margin: 15px; }
不過這種方式也是存在缺陷的,譬如你無法設置圖片的懶加載、圖片無法被搜索引擎或者其他類似的工具抓取到,有個不錯的屬性叫object-fit可以解決這個問題,不過該屬性目前的瀏覽器支持并不是很完善。
Better Table Borders
HTML中使用Tables進行布局一直是個很頭疼的問題,它們使用起來很簡單,但是無法進行響應式操作,并且也不方便進行全局樣式設置。譬如,如果你打算為Table的邊與單元的邊添加樣式,可能得到的結果如下:
table { width: 600px; border: 1px solid #505050; margin-bottom: 15px; color:#505050; }td{ border: 1px solid #505050; padding: 10px; }
這里存在的問題是出現了很多的重復的邊,會導致視覺上不協調的情況,那么我們可以通過設置border-collapse:collapse
來進行處理:
注釋格式優化
CSS雖然談不上一門編程語言但是其仍然需要添加注釋以保障整體代碼的可讀性,只要添加些簡單的注釋不僅可以方便你更好地組織整個樣式表還能夠讓你的同事或者未來的自己更好地理解。對于CSS中整塊的注釋或者使用在Media-Query中的注釋,建議是使用如下形式:
/*--------------- #Header ---------------*/header { }header nav { }/*--------------- #Slideshow ---------------*/.slideshow { }
而設計的細節說明或者一些不重要的組件可以用如下單行注釋的方式:
/* Footer Buttons */.footer button { }.footer button:hover { }
同時,不要忘了CSS中是沒有//
這種注釋方式的:
/* Do */p { padding: 15px; /*border: 1px solid #222;*/}/* Don't */p { padding: 15px; // border: 1px solid #222; }
使用Kebab-case命名變量
對于樣式類名或者ID名的命名都需要在多個單詞之間添加-
符號,CSS本身是大小寫不敏感的因此你是用不了camelCase的,另一方面,很久之前也不支持下劃線,所以現在的默認的命名方式就是使用-
:
/* Do */.footer-column-left { }/* Don't */.footerColumnLeft { }.footer_column_left { }
而涉及到具體的變量命名規范時,建議是使用BEM規范,只要遵循一些簡單的原則即可以保證基于組件風格的命名一致性。你也可以參考CSS Tricks來獲得更多的細節描述。
避免重復代碼
大部分元素的CSS屬性都是從DOM樹根部繼承而來,這也是其命名為級聯樣式表的由來。我們以font
屬性為例,該屬性往往是繼承自父屬性,因此我們并不需要再單獨地為元素設置該屬性。我們只需要在html
或者body
中添加該屬性然后使其層次傳遞下去即可:
html { font: normal 16px/1.4 sans-serif; }
使用transform添加CSS Animations
不建議直接改變元素的width
與height
屬性或者left/top/bottom/right
這些屬性來達到動畫效果,而應該優先使用transform()
屬性來提供更平滑的變換效果,并且能使得代碼的可讀性會更好:
.ball { left: 50px; transition: 0.4s ease-out; }/* Not Cool*/.ball.slide-out { left: 500px; }/* Cool*/.ball.slide-out { transform: translateX(450px); }
Transform的幾個屬性translate
、rotate
、scale
都具有比較好的瀏覽器兼容性可以放心使用。
不要重復造輪子
現在CSS社區已經非常龐大,并且不斷地有新的各式各樣的庫開源出來。這些庫可以幫助我們解決從小的代碼片到用于構建完整的響應式應用的全框架。所以如果下次你再碰到什么CSS問題的時候,在打算擼起袖子自己上之前可以嘗試在GitHUB或者CodePen上搜索可行方案。
盡可能使用低優先級的選擇器
并不是所有的CSS選擇器的優先級都一樣,很多初學者在使用CSS選擇器的時候都是考慮以新的特性去復寫全部的繼承特性,不過這一點在某個元素多狀態時就麻煩了,譬如下面這個例子:
a{ color: #fff; padding: 15px; }a#blue-btn { background-color: blue; }a.active { background-color: red; }
我們本來希望將.active
類添加到按鈕上然后使其顯示為紅色,不過在上面這個例子中很明顯起不了作用,因為button
已經以ID選擇器設置過了背景色,也就是所謂的Higher Selector Specificity。一般來說,選擇器的優先級順序為:ID(#id) > Class(.class) > Type(header)
避免使用!important
認真的說,千萬要避免使用!important,這可能會導致你在未來的開發中無盡的屬性重寫,你應該選擇更合適的CSS選擇器。而唯一的可以使用!important
屬性的場景就是當你想去復寫某些行內樣式的時候,不過行內樣式本身也是需要避免的。
使用text-transform屬性設置文本大寫
<div class="movie-poster">Star Wars: The Force Awakens</div>.movie-poster { text-transform: uppercase; }
Em, Rem, 以及 Pixel
已經有很多關于人們應該如何使用em,rem,以及px作為元素尺寸與文本尺寸的討論,而筆者認為,這三個尺寸單位都有其適用與不適用的地方。不同的開發與項目都有其特定的設置,因此并沒有通用的規則來決定應該使用哪個單位,這里是我總結的幾個考慮:
em – 其基本單位即為當前元素的font-size
值,經常適用于media-queries中,em是特別適用于響應式開發中。
rem – 其是相對于html
屬性的單位,可以保證文本段落真正的響應式尺寸特性。
px – Pixels 并沒有任何的動態擴展性,它們往往用于描述絕對單位,并且可以在設置值與最終的顯示效果之間保留一定的一致性。
在大型項目中使用預處理器
估計你肯定聽說過 Sass, Less, PostCSS, Stylus這些預處理器與對應的語法。Preprocessors可以允許我們將未來的CSS特性應用在當前的代碼開發中,譬如變量支持、函數、嵌套式的選擇器以及很多其他的特性,這里我們以Sass為例:
$accent-color: #2196F3;a { padding: 10px 15px; background-color: $accent-color; }a:hover { background-color: darken($accent-color,10%); }
使用Autoprefixers來提升瀏覽器兼容性
使用特定的瀏覽器前綴是CSS開發中常見的工作之一,不同的瀏覽器、不同的屬性對于前綴的要求也不一樣,這就使得我們無法在編碼過程中記住所有的前綴規則。并且在寫樣式代碼的時候還需要加上特定的瀏覽器前綴支持也是個麻煩活,幸虧現在也是有很多工具可以輔助我們進行這樣的開發:
Online tools: Autoprefixer
Text editor plugins: Sublime Text, Atom
Libraries: Autoprefixer (PostCSS)
在生產環境下使用Minified代碼
為了提升頁面的加載速度,在生產環境下我們應該默認使用壓縮之后的資源代碼。在壓縮的過程中,會將所有的空白與重復剔除掉從而減少整個文件的體積大小。當然,經過壓縮之后的代碼毫無可讀性,因此在開發階段我們還是應該使用普通的版本。對于CSS的壓縮有很多的現行工具:
Online tools – CSS Minifier (API included), CSS Compressor
Text editor plugins: Sublime Text, Atom
Libraries: Minfiy (PHP), CSSO and CSSNano (PostCSS, Grunt, Gulp)
選擇哪個工具肯定是依賴于你自己的工作流啦~
多參閱Caniuse
不同的瀏覽器在兼容性上差異很大,因此如果我們可以針對我們所需要適配的瀏覽器,在caniuse上我們可以查詢某個特性的瀏覽器版本適配性,是否需要添加特定的前綴或者在某個平臺上是否存在Bug等等。不過光光使用caniuse肯定是不夠的,我們還需要使用些額外的服務來進行檢測。
Validate:校驗
對于CSS的校驗可能不如HTML校驗或者JavaScript校驗那么重要,不過在正式發布之前用Lint工具校驗一波你的CSS代碼還是很有意義的。它會告訴你代碼中潛在的錯誤,提示你一些不符合最佳實踐的代碼以及給你一些提升代碼性能的建議。就像Minifers與Autoprefixers,也有很多可用的工具:
Online tools: W3 Validator, CSS Lint
Text editor plugins: Sublime Text, Atom
Libraries: lint (Node.js, PostCSS), css-validator (Node.js)
(作者:Danny Markov,翻譯:王下邀月熊_Chevalier)
英語原文:20 Protips For Writing Modern CSS
迎大家關注我,我會不定期分享一些自己覺得比較好的文章給大家。
【摘要】本文為 Google Chrome 團隊的開發項目工程師 Addy Osmani 在PerfMatters 2019 網頁性能大會發表的“JavaScript性能優化”(https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4)的演講,其分享了處理 JavaScript 的腳本優化建議,大幅地減少了下載時間和執行時間。
以下為譯文:
在過去的幾年中,由于瀏覽器的腳本解析和編譯速度的提高,Javascript成本構成發生了巨大的變化。到了2019年,處理Javascript的開銷主要體現在腳本下載時間和CPU執行時間上。
如果瀏覽器的主線程忙于執行Javascript腳本,則用戶交互體驗可能會受影響,因此,優化腳本執行時間并消除網絡瓶頸,會對用戶體驗產生積極的作用。
高層級的實用指南
這對Web開發人員來說意味著什么?意味著解析(Parse)和編譯(Compile)不再像我們曾經想象的那么慢了。所以開發人員在優化Javascript包時,要重點關注以下三大方面:
減少下載時間
縮短執行時間
避免使用大型內聯腳本(因為它們仍然需要在主線程上進行解析和編譯)。
為什么下載和執行時間很重要?
為什么優化下載和執行時間對我們很重要?因為對于低端網絡而言,下載時間的影響非常之大。盡管4G(甚至5G)在全球范圍內增長迅速,但大多數人的有效連接速度仍然遠遠低于網絡的標稱速度。有時當我們外出時,會感覺到網速下降到只有3G的速度(甚至更糟)。
JavaScript的執行時間對于CPU較慢的低端手機也非常重要。由于CPU、GPU,和散熱限制的不同,高端和低端手機的性能差距巨大。這對JavaScript的性能影響明顯,因為它的執行受到CPU性能的制約。
事實上,在Chrome之類的瀏覽器上,JavaScript的執行時間可以達到頁面加載總耗時的30%。下圖是一個具有典型工作負載的網站(Reddit.com)在一臺高端桌面PC上的頁面加載情況分析:
V8引擎下的Javascript處理時間占整個頁面加載時間的10-30%
對于移動設備,與高端手機(如Pixel 3)相比,在中端手機(如Moto G4)上執行Reddit的Javascript腳本需要3-4倍的耗時,而在低端手機(價格低于100美元的Alcatel 1X)上執行Reddit的Javascript腳本更是需要6倍以上的耗時:
Reddit的Javascript腳本在幾種不同設備(低端、中端和高端)上的執行時間。
注意:Reddit對于桌面和移動網絡有不同的體驗,因此MacBook Pro的執行結果無法與其他結果進行比較。
當你著手優化JavaScript的執行時間時,你需要留意可能長時間獨占界面線程(UI Thread)的長時任務。即使頁面看起來已經加載完成,這些長時任務也會拖累關鍵任務的執行。把長時任務分解成較小的任務。通過拆分代碼并確定加載順序,你可以更快地實現頁面交互,并有望降低輸入延遲。
獨占主線程的長時任務應該拆分。
V8引擎如何提高Javascript解析/編譯速度?
自Chrome 版本60以來,V8引擎的原始JS的解析速度增加了2倍。與此同時,Chrome還做了其他工作一些工作使得解析和編譯工作并行化,這使得這部分的成本開銷對用戶體驗的影響變得不是那么顯著和關鍵了。
V8引擎通過將解析和編譯工作轉到worker線程上,使得主線程上的解析和編譯工作量平均減少了40%。例如,Facebook降低了46%,Pinterest降低62%,而最大的改進是是YouTube ,降低了81%。這是在現有的非主線程流解析/編譯性能改進基礎上的進一步提升。
不同版本的V8引擎的解析時間對比
我們還可以圖示對比不同Chrome版本的不同V8引擎對CPU處理時間的影響。可以看出,Chrome 61解析Facebook的JS腳本所花費的時間,可以供Chrome 75解析同樣的Facebook的JS腳本,和6個Twitter的JS腳本了。
Chrome 61解析Facebook的JS腳本所花費的時間,可以供Chrome 75解析完成同樣的Facebook的JS腳本,和6個Twitter的JS腳本了。
讓我們深入研究一下這些改進是如何實現的。總的來說,腳本資源可以在worker線程上進行流式解析和編譯,這意味著:
具體來說,很多老版本的Chrome在開始腳本解析之前,需要將腳本下載完成,這是一種簡單的方法,但它沒有充分利用CPU的能力。而從版本41到68,Chrome在下載一開始時就立即在單獨的線程上解析異步和延遲腳本。
JS腳本以多個塊下載。V8引擎看到大于30KB的腳本被下載后就會啟動腳本流解析工作。
Chrome 71采用了基于任務(task-based)的設置方案。調度器可以一次解析多個異步/延遲腳本,這一改進使得主線程解析時間縮短了約20%,真實網站上的TTI/FID整體提高了大約2%。
Chrome 71采用了基于任務(task-based)的設置,調度器可以一次解析多個異步/延遲腳本
Chrome 72開始采用流式處理作為主要的解析方式,現在常規的同步腳本(內聯腳本除外)也可以采用這種解析方式。如果主線程需要,我們也可以繼續采用基于任務的解析,從而減少不必要地重復工作。
舊版的Chrome支持流式解析和編譯,其中來自網絡的腳本源數據必須先到達Chrome主線程后,再轉發給流解析器解析。
這通常會導致這樣的情況:腳本數據已經從網絡上下載完成,但由于主線程上的其他任務(如HTML解析、排版或者JavaScript執行),阻塞了腳本數據的轉發,因此流解析器(streaming parser)不得不空等。
現在我們正嘗試在預加載時開始解析,以前主線程反彈會阻礙這種操作。
Leszek Swirski 在 BlinkOn 10 上的演講介紹了相關細節:https://youtu.be/D1UJgiG4_NI(需科學上網)
這些改變如何反映到DevTools中?
除上述之外,DevTools中還存在一個問題,它以表明它會獨占 CPU(完全阻塞)的方式渲染整個解析器任務。但是,不管解析器是否需要數據(數據需要通過主線程)都會阻塞。當我我們從單個流線程轉向多個流傳輸任務時,這個問題變得非常明顯。下面是你在Chrome 69中看到的情況:
DevTools以表明它會獨占CPU(完全阻塞)的方式渲染整個解析器任務
如上圖示,“解析腳本”任務需要1.08秒。但是解析JavaScript其實并沒有那么慢!大部分時間除了等待數據通過主線程之外什么都做不了。
而在Chrome 76中顯示的內容就不一樣了:
在Chrome 76中,解析工作被分解為多個較小的流任務。
一般來說,DevTools性能窗格非常適合從宏觀層面分析你的頁面。對于更具體的V8度量指標,如Javascript解析和編譯時間,我們建議使用帶有運行時調用統計(RCS)的Chrome跟蹤工具。在RCS結果中,Parse-Background和Compile-Background會告訴你在主線程外解析和編譯Javascript花費了多少時間,而Parse和Compile是針對主線程的度量指標。
這些改變對現實應用的影響是什么?
讓我們來看一些真實網站的示例,來了解腳本流(script streaming)是如何工作的。
主線程和worker線程在MacBook Pro上解析和編譯Reddit網站的JS所花費的時間對比
Reddit.com網站有幾個超過100KB的JS包,它們包裝在外部函數中,導致在主線程上需要進行大量的延遲編譯(lazy compilation)。如上圖所示,主線程耗時才是真正關鍵的,因為主線程持續繁忙會嚴重影響交互體驗。Reddit的大部分時間花在了主線程上,而worker線程或后臺線程的使用率很低。
可以將一些較大的JS包拆分為幾個不需要包裝的小包(例如每個包50 KB),以最大限度地實現并行化,這樣每個包都可以單獨進行流解析和編譯,并在載入期間減少主線程的解析/編譯時間。
主線程和worker線程在MacBook Pro上解析和編譯Facebook網站的JS所花費的時間對比
我們再看看像facebook.com這樣的網站的情況。Facebook使用了大約292個請求,加載了大約6MB的壓縮JS腳本,其中一些是異步的,一些是預加載的,還有一些是低優先級的。它們的許多腳本都非常小,粒度也不大,這有助于后臺/workers線程上的整體并行化,因為這些較小的腳本可以同時進行流解析/編譯。
值得注意地是,像Facebook或Gmail這樣老牌的應用程序的桌面版本上有這么多的腳本可能是合理的。但是你的網站可能和Facebook不一樣。不管怎樣,盡可能地簡化你的JS包,不必要的就不要裝載了。
盡管大多數JavaScript解析和編譯工作都可以在后臺線程上以流式方式進行,但仍有一些工作必須在主線程上進行。而當主線程繁忙時,頁面就無法響應用戶輸入了。所以要密切關注下載和執行代碼對用戶體驗的影響。
注意:目前并不是所有的Javascript引擎和瀏覽器都實現了腳本流(script streaming)式加載優化。但是我們仍然相信,本文的整體指導會幫助大家全面地提升用戶體驗。
解析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方法就要快得多,冷加載時尤其明顯。
在為大量數據使用普通對象文本時還有一個額外的風險:它們可能會被解析兩次!
第一次解析是必須的,可以將對象文本放在頂層或PIFE中來避免第二次解析。
重復訪問時的解析/編譯情況如何?
V8引擎的(字節)代碼緩存優化可以幫助改善重復訪問時的體驗。當第一次請求腳本時,Chrome會下載腳本并將其交給V8引擎進行編譯。同時將文件存儲在瀏覽器的磁盤緩存中。當第二次請求JS文件時,Chrome會從瀏覽器緩存中獲取該文件,并再次將其交給V8引擎進行編譯。然而,這次編譯的代碼會被序列化,并作為元數據附加到緩存的腳本文件中。
V8引擎的代碼緩存示意圖
第三次請求腳本時,Chrome從緩存中獲取腳本文件和文件的元數據,并將兩者都交給V8引擎。V8引擎會反序列化元數據來跳過編譯步驟。如果前兩次訪問間隔小于72小時內,代碼緩存就會啟動。如果采用service worker來緩存腳本,那么chrome也會主動啟動代碼緩存。詳細信息可以參閱 web 開發者的代碼緩存指南。
總結
到了2019年。腳本下載和執行的時間開銷已經變成加載腳本的主要瓶頸。所以你應該為你的首屏內容準備一個較小的同步(內聯)腳本包,其余部分則使用一個或多個延遲腳本,并且把較大的包拆分成許多小包來按需加載。這樣一來就能充分利用 V8 引擎的并行化能力。
在移動設備上,由于網絡、內存消耗和CPU執行時間的制約,你需要盡可能地減少腳本的數量,平衡延遲和緩存設置,盡可能地讓解析和編譯工作在主線程外執行。
原文:
https://v8.dev/blog/cost-of-javascript-2019
*請認真填寫需求信息,我們會在24小時內與您取得聯系。