整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          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(需科學上網)

          作者 | 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方法就要快得多,冷加載時尤其明顯。

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

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

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

          第一次解析是必須的,可以將對象文本放在頂層或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】

          、廣告代碼分析
          很多第三方地廣告系統都是使用document.write來加載廣告,如下面地一個javascript地廣告鏈接.

          代碼如下:

          者 | SHERlocked93

          責編 | 胡巍巍

          大多數設備的刷新頻率是60Hz,也就說是瀏覽器對每一幀畫面的渲染工作要在16ms內完成,超出這個時間,頁面的渲染就會出現卡頓現象,影響用戶體驗。前端的用戶體驗給了前端直觀的印象,因此對B/S架構的開發人員來說,熟悉瀏覽器的內部執行原理顯得尤為重要。

          瀏覽器主要組成與瀏覽器線程

          1.1 瀏覽器組件

          瀏覽器大體上由以下幾個組件組成,各個瀏覽器可能有一點不同。

          • 界面控件 – 包括地址欄,前進后退,書簽菜單等窗口上除了網頁顯示區域以外的部分

          • 瀏覽器引擎 – 查詢與操作渲染引擎的接口

          • 渲染引擎 – 負責顯示請求的內容。比如請求到HTML, 它會負責解析HTML、CSS并將結果顯示到窗口中

          • 網絡 – 用于網絡請求, 如HTTP請求。它包括平臺無關的接口和各平臺獨立的實現

          • UI后端 – 繪制基礎元件,如組合框與窗口。它提供平臺無關的接口,內部使用操作系統的相應實現

          • JS解釋器 - 用于解析執行JavaScript代碼

          • 數據存儲持久層 - 瀏覽器需要把所有數據存到硬盤上,如cookies。新的HTML5規范規定了一個完整(雖然輕量級)的瀏覽器中的數據庫 web database

          注意:chrome瀏覽器與其他瀏覽器不同,chrome使用多個渲染引擎實例,每個Tab頁一個,即每個Tab都是一個獨立進程。

          1.2 瀏覽器中的進程與線程

          Chrome瀏覽器使用多個進程來隔離不同的網頁,在Chrome中打開一個網頁相當于起了一個進程,每個tab網頁都有由其獨立的渲染引擎實例。因為如果非多進程的話,如果瀏覽器中的一個tab網頁崩潰,將會導致其他被打開的網頁應用。另外相對于線程,進程之間是不共享資源和地址空間的,所以不會存在太多的安全問題,而由于多個線程共享著相同的地址空間和資源,所以會存在線程之間有可能會惡意修改或者獲取非授權數據等復雜的安全問題。

          在內核控制下各線程相互配合以保持同步,一個瀏覽器通常由以下常駐線程組成:

          1. GUI 渲染線程

          GUI渲染線程負責渲染瀏覽器界面HTML元素,當界面需要重繪(Repaint)或由于某種操作引發回流(reflow)時,該線程就會執行。在Javascript引擎運行腳本期間,GUI渲染線程都是處于掛起狀態的,也就是說被凍結了.

          2. JavaScript引擎線程

          JS為處理頁面中用戶的交互,以及操作DOM樹、CSS樣式樹來給用戶呈現一份動態而豐富的交互體驗和服務器邏輯的交互處理。如果JS是多線程的方式來操作這些UI DOM,則可能出現UI操作的沖突;如果JS是多線程的話,在多線程的交互下,處于UI中的DOM節點就可能成為一個臨界資源,假設存在兩個線程同時操作一個DOM,一個負責修改一個負責刪除,那么這個時候就需要瀏覽器來裁決如何生效哪個線程的執行結果,當然我們可以通過鎖來解決上面的問題。但為了避免因為引入了鎖而帶來更大的復雜性,JS在最初就選擇了單線程執行。

          GUI渲染線程與JS引擎線程互斥的,是由于JavaScript是可操縱DOM的,如果在修改這些元素屬性同時渲染界面(即JavaScript線程和UI線程同時運行),那么渲染線程前后獲得的元素數據就可能不一致。當JavaScript引擎執行時GUI線程會被掛起,GUI更新會被保存在一個隊列中等到引擎線程空閑時立即被執行。由于GUI渲染線程與JS執行線程是互斥的關系,當瀏覽器在執行JS程序的時候,GUI渲染線程會被保存在一個隊列中,直到JS程序執行完成,才會接著執行。因此如果JS執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染加載阻塞的感覺。

          3. 定時觸發器線程

          瀏覽器定時計數器并不是由JS引擎計數的, 因為JS引擎是單線程的, 如果處于阻塞線程狀態就會影響記計時的準確, 因此通過單獨線程來計時并觸發定時是更為合理的方案。

          4. 事件觸發線程

          當一個事件被觸發時該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理。這些事件可以是當前執行的代碼塊如定時任務、也可來自瀏覽器內核的其他線程如鼠標點擊、AJAX異步請求等,但由于JS的單線程關系所有這些事件都得排隊等待JS引擎處理。

          5. 異步http請求線程

          在XMLHttpRequest在連接后是通過瀏覽器新開一個線程請求,將檢測到狀態變更時,如果設置有回調函數,異步線程就產生狀態變更事件放到JS引擎的處理隊列中等待處理。

          渲染過程

          2.1 渲染流程

          用戶請求的HTML文本(text/html)通過瀏覽器的網絡層到達渲染引擎后,渲染工作開始。每次通常渲染不會超過8K的數據塊,其中基礎的渲染流程圖:

          webkit引擎渲染的詳細流程,其他引擎渲染流程稍有不同:

          渲染流程有四個主要步驟:

          1. 解析HTML生成DOM樹 - 渲染引擎首先解析HTML文檔,生成DOM樹

          2. 構建Render樹 - 接下來不管是內聯式,外聯式還是嵌入式引入的CSS樣式會被解析生成CSSOM樹,根據DOM樹與CSSOM樹生成另外一棵用于渲染的樹-渲染樹(Render tree),

          3. 布局Render樹 - 然后對渲染樹的每個節點進行布局處理,確定其在屏幕上的顯示位置

          4. 繪制Render樹 - 最后遍歷渲染樹并用UI后端層將每一個節點繪制出來

          以上步驟是一個漸進的過程,為了提高用戶體驗,渲染引擎試圖盡可能快的把結果顯示給最終用戶。它不會等到所有HTML都被解析完才創建并布局渲染樹。它會在從網絡層獲取文檔內容的同時把已經接收到的局部內容先展示出來。

          2.2 渲染細節

          1. 生成DOM樹

          DOM樹的構建過程是一個深度遍歷過程:當前節點的所有子節點都構建好后才會去構建當前節點的下一個兄弟節點。DOM樹的根節點就是document對象。

          DOM樹的生成過程中可能會被CSS和JS的加載執行阻塞,具體可以參見下一章。當HTML文檔解析過程完畢后,瀏覽器繼續進行標記為deferred模式的腳本加載,然后就是整個解析過程的實際結束觸發DOMContentLoaded事件,并在async文檔文檔執行完之后觸發load事件。

          2. 生成Render樹

          生成DOM樹的同時會生成樣式結構體CSSOM(CSS Object Model)Tree,再根據CSSOM和DOM樹構造渲染樹Render Tree,渲染樹包含帶有顏色,尺寸等顯示屬性的矩形,這些矩形的順序與顯示順序基本一致。從MVC的角度來說,可以將Render樹看成是V,DOM樹與CSSOM樹看成是M,C則是具體的調度者,比HTMLDocumentParser等。

          可以這么說,沒有DOM樹就沒有Render樹,但是它們之間不是簡單的一對一的關系。Render樹是用于顯示,那不可見的元素當然不會在這棵樹中出現了,譬如 <head>。除此之外,display等于none的也不會被顯示在這棵樹里頭,但是visibility等于hidden的元素是會顯示在這棵樹里頭的。

          3. DOM樹與Render樹

          DOM對象類型很豐富,什么head、title、div,而Render樹相對來說就比較單一了,畢竟它的職責就是為了以后的顯示渲染用嘛。Render樹的每一個節點我們叫它渲染器renderer。

          一棵Render樹大概是醬紫,左邊是DOM樹,右邊是Render樹:

          從上圖我們可以看出,renderer與DOM元素是相對應的,但并不是一一對應,有些DOM元素沒有對應的renderer,而有些DOM元素卻對應了好幾個renderer,對應多個renderer的情況是普遍存在的,就是為了解決一個renderer描述不清楚如何顯示出來的問題,譬如有下拉列表的select元素,我們就需要三個renderer:一個用于顯示區域,一個用于下拉列表框,還有一個用于按鈕。

          另外,renderer與DOM元素的位置也可能是不一樣的。那些添加了 float或者 position:absolute的元素,因為它們脫離了正常的文檔流,構造Render樹的時候會針對它們實際的位置進行構造。

          4. 布局與繪制

          上面確定了renderer的樣式規則后,然后就是重要的顯示元素布局了。當renderer構造出來并添加到Render樹上之后,它并沒有位置跟大小信息,為它確定這些信息的過程,接下來是布局(layout)。

          瀏覽器進行頁面布局基本過程是以瀏覽器可見區域為畫布,左上角為 (0,0)基礎坐標,從左到右,從上到下從DOM的根節點開始畫,首先確定顯示元素的大小跟位置,此過程是通過瀏覽器計算出來的,用戶CSS中定義的量未必就是瀏覽器實際采用的量。如果顯示元素有子元素得先去確定子元素的顯示信息。

          布局階段輸出的結果稱為box盒模型(width,height,margin,padding,border,left,top,…),盒模型精確表示了每一個元素的位置和大小,并且所有相對度量單位此時都轉化為了絕對單位。

          在繪制(painting)階段,渲染引擎會遍歷Render樹,并調用renderer的 paint 方法,將renderer的內容顯示在屏幕上。繪制工作是使用UI后端組件完成的。

          5. 回流與重繪

          回流(reflow):當瀏覽器發現某個部分發生了點變化影響了布局,需要倒回去重新渲染。reflow 會從 <html>這個 root frame 開始遞歸往下,依次計算所有的結點幾何尺寸和位置。reflow 幾乎是無法避免的。現在界面上流行的一些效果,比如樹狀目錄的折疊、展開(實質上是元素的顯示與隱藏)等,都將引起瀏覽器的 reflow。鼠標滑過、點擊……只要這些行為引起了頁面上某些元素的占位面積、定位方式、邊距等屬性的變化,都會引起它內部、周圍甚至整個頁面的重新渲染。通常我們都無法預估瀏覽器到底會 reflow 哪一部分的代碼,它們都彼此相互影響著。

          重繪(repaint):改變某個元素的背景色、文字顏色、邊框顏色等等不影響它周圍或內部布局的屬性時,屏幕的一部分要重畫,但是元素的幾何尺寸沒有變。

          每次Reflow,Repaint后瀏覽器還需要合并渲染層并輸出到屏幕上。所有的這些都會是動畫卡頓的原因。Reflow 的成本比 Repaint 的成本高得多的多。一個結點的 Reflow 很有可能導致子結點,甚至父點以及同級結點的 Reflow 。在一些高性能的電腦上也許還沒什么,但是如果 Reflow 發生在手機上,那么這個過程是延慢加載和耗電的。可以在csstrigger上查找某個css屬性會觸發什么事件。

          reflow與repaint的時機:

          1. display:none 會觸發 reflow,而 visibility:hidden 只會觸發 repaint,因為沒有發生位置變化。

          2. 有些情況下,比如修改了元素的樣式,瀏覽器并不會立刻 reflow 或 repaint 一次,而是會把這樣的操作積攢一批,然后做一次 reflow,這又叫異步 reflow 或增量異步 reflow。

          3. 有些情況下,比如 resize 窗口,改變了頁面默認的字體等。對于這些操作,瀏覽器會馬上進行 reflow。

          關鍵渲染路徑與阻塞渲染

          在瀏覽器拿到HTML、CSS、JS等外部資源到渲染出頁面的過程,有一個重要的概念關鍵渲染路徑(Critical Rendering Path)。例如為了保障首屏內容的最快速顯示,通常會提到一個漸進式頁面渲染,但是為了漸進式頁面渲染,就需要做資源的拆分,那么以什么粒度拆分、要不要拆分,不同頁面、不同場景策略不同。具體方案的確定既要考慮體驗問題,也要考慮工程問題。了解原理可以讓我們更好的優化關鍵渲染路徑,從而獲得更好的用戶體驗。

          現代瀏覽器總是并行加載資源,例如,當 HTML 解析器(HTML Parser)被腳本阻塞時,解析器雖然會停止構建 DOM,但仍會識別該腳本后面的資源,并進行預加載。

          同時,由于下面兩點:

          1. CSS 被視為渲染 阻塞資源 (包括JS) ,這意味著瀏覽器將不會渲染任何已處理的內容,直至 CSSOM 構建完畢,才會進行下一階段。

          2. JavaScript 被認為是解釋器阻塞資源,HTML解析會被JS阻塞,它不僅可以讀取和修改 DOM 屬性,還可以讀取和修改 CSSOM 屬性。

          存在阻塞的 CSS 資源時,瀏覽器會延遲 JavaScript 的執行和 DOM 構建。另外:

          1. 當瀏覽器遇到一個 script 標記時,DOM 構建將暫停,直至腳本完成執行。

          2. JavaScript 可以查詢和修改 DOM 與 CSSOM。

          3. CSSOM 構建時,JavaScript 執行將暫停,直至 CSSOM 就緒。

          所以,script 標簽的位置很重要。實際使用時,可以遵循下面兩個原則:

          1. CSS 優先:引入順序上,CSS 資源先于 JavaScript 資源。

          2. JavaScript 應盡量少影響 DOM 的構建。

          下面來看看 CSS 與 JavaScript 是具體如何阻塞資源的。

          3.1 CSS

          <style> p { color: red; }</style>

          <link rel="stylesheet" href="index.css">

          這樣的 link 標簽(無論是否 inline)會被視為阻塞渲染的資源,瀏覽器會優先處理這些 CSS 資源,直至 CSSOM 構建完畢。

          渲染樹(Render-Tree)的關鍵渲染路徑中,要求同時具有 DOM 和 CSSOM,之后才會構建渲染樹。即,HTML 和 CSS 都是阻塞渲染的資源。HTML 顯然是必需的,因為包括我們希望顯示的文本在內的內容,都在 DOM 中存放,那么可以從 CSS 上想辦法。

          最容易想到的當然是精簡 CSS 并盡快提供它。除此之外,還可以用媒體類型(media type)和媒體查詢(media query)來解除對渲染的阻塞。

          <link href="index.css" rel="stylesheet">

          <link href="print.css" rel="stylesheet" media="print">

          <link href="other.css" rel="stylesheet" media="(min-width: 30em) and (orientation: landscape)">

          第一個資源會加載并阻塞。第二個資源設置了媒體類型,會加載但不會阻塞,print 聲明只在打印網頁時使用。第三個資源提供了媒體查詢,會在符合條件時阻塞渲染。

          關于CSS加載的阻塞情況:

          1. css加載不會阻塞DOM樹的解析

          2. css加載會阻塞DOM樹的渲染

          3. css加載會阻塞后面js語句的執行

          沒有js的理想情況下,html與css會并行解析,分別生成DOM與CSSOM,然后合并成Render Tree,進入Rendering Pipeline;但如果有js,css加載會阻塞后面js語句的執行,而(同步)js腳本執行會阻塞其后的DOM解析(所以通常會把css放在頭部,js放在body尾)

          3.2 JavaScript

          JavaScript 的情況比 CSS 要更復雜一些。如果沒有 defer 或 async,瀏覽器會立即加載并執行指定的腳本,“立即”指的是在渲染該 script 標簽之下的HTML元素之前,也就是說不等待后續載入的HTML元素,讀到就加載并執行。觀察下面的代碼:

          <p>Do not go gentle into that good night,</p>

          <script>console.log("inline1")</script>

          <p>Old age should burn and rave at close of day;</p>

          <script src="app.js"></script>

          <p>Rage, rage against the dying of the light.</p>

          <script src="app.js"></script>

          <p>Old age should burn and rave at close of day;</p>

          <script>console.log("inline2")</script>

          <p>Rage, rage against the dying of the light.</p>

          這里的 script 標簽會阻塞 HTML 解析,無論是不是 inline-script。上面的 P 標簽會從上到下解析,這個過程會被兩段 JavaScript 分別打斷一次(加載、執行)。

          解析過程中無論遇到的JavaScript是內聯還是外鏈,只要瀏覽器遇到 script 標記,喚醒 JavaScript解析器,就會進行暫停 (blocked )瀏覽器解析HTML,并等到 CSSOM 構建完畢,才去執行js腳本。因為腳本中可能會操作DOM元素,而如果在加載執行腳本的時候DOM元素并沒有被解析,腳本就會因為DOM元素沒有生成取不到響應元素,所以實際工程中,我們常常將資源放到文檔底部。

          3.3 改變腳本加載次序defer與async

          defer 與 async 可以改變之前的那些阻塞情形,這兩個屬性都會使 script 異步加載,然而執行的時機是不一樣的。注意 async 與 defer 屬性對于 inline-script 都是無效的,所以下面這個示例中三個 script 標簽的代碼會從上到下依次執行。

          <script async>console.log("1")</script>

          <script defer>console.log("2")</script>

          <script>console.log("3")</script>

          上面腳本會按需輸出 1 2 3,故,下面兩節討論的內容都是針對設置了 src 屬性的 script 標簽。

          先放個熟悉的圖~

          藍色線代表網絡讀取,紅色線代表執行時間,這倆都是針對腳本的;綠色線代表 HTML 解析。

          defer:

          <script src="app1.js" defer></script>

          <script src="app2.js" defer></script>

          <script src="app3.js" defer></script>

          defer 屬性表示延遲執行引入 JavaScript,即 JavaScript 加載時 HTML 并未停止解析,這兩個過程是并行的。整個 document 解析完畢且 defer-script 也加載完成之后(這兩件事情的順序無關),會執行所有由 defer-script 加載的 JavaScript 代碼,再觸發 DOMContentLoaded(初始的 HTML 文檔被完全加載和解析完成之后觸發,無需等待樣式表圖像和子框架的完成加載) 事件 。

          defer 不會改變 script 中代碼的執行順序,示例代碼會按照 1、2、3 的順序執行。所以,defer 與相比普通 script,有兩點區別:載入 JavaScript 文件時不阻塞 HTML 的解析,執行階段被放到 HTML 標簽解析完成之后。

          async:

          async 屬性表示異步執行引入的 JavaScript,與 defer 的區別在于,如果已經加載好,就會開始執行,無論此刻是 HTML 解析階段還是 DOMContentLoaded 觸發(HTML解析完成事件)之后。需要注意的是,這種方式加載的 JavaScript 依然會阻塞 load 事件。換句話說,async-script 可能在 DOMContentLoaded 觸發之前或之后執行,但一定在 load 觸發之前執行。

          從上一段也能推出,多個 async-script 的執行順序是不確定的,誰先加載完誰執行。值得注意的是,向 document 動態添加 script 標簽時,async 屬性默認是 true。

          document.createElement:

          使用 document.createElement 創建的 script 默認是異步的,示例如下。

          console.log(document.createElement("script").async); // true

          所以,通過動態添加 script 標簽引入 JavaScript 文件默認是不會阻塞頁面的。如果想同步執行,需要將 async 屬性人為設置為 false。

          如果使用 document.createElement 創建 link 標簽會怎樣呢?

          const style = document.createElement("link");

          style.rel = "stylesheet";

          style.href = "index.css";

          document.head.appendChild(style); // 阻塞?

          其實這只能通過試驗確定,已知的是,Chrome 中已經不會阻塞渲染,Firefox、IE 在以前是阻塞的,現在會怎樣目前不太清楚。

          優化渲染性能

          結合渲染流程,可以針對性的優化渲染性能:

          1. 優化JS的執行效率

          2. 降低樣式計算的范圍和復雜度

          3. 避免大規模、復雜的布局

          4. 簡化繪制的復雜度、減少繪制區域

          5. 優先使用渲染層合并屬性、控制層數量

          6. 對用戶輸入事件的處理函數去抖動(移動設備)

          這里主要參考Google的瀏覽器渲染性能的基礎講座,想看更詳細內容可以去瞅瞅~

          4.1 優化JS的執行效率

          1. 動畫實現使用requestAnimationFrame

          setTimeout(callback)和setInterval(callback)無法保證callback函數的執行時機,很可能在幀結束的時候執行,從而導致丟幀,如下圖:

          requestAnimationFrame(callback)可以保證callback函數在每幀動畫開始的時候執行。注意:jQuery3.0.0以前版本的animate函數就是用setTimeout來實現動畫,可以通過jquery-requestAnimationFrame這個補丁來用requestAnimationFrame替代setTimeout

          2. 長耗時的JS代碼放到Web Workers中執行

          JS代碼運行在瀏覽器的主線程上,與此同時,瀏覽器的主線程還負責樣式計算、布局、繪制的工作,如果JavaScript代碼運行時間過長,就會阻塞其他渲染工作,很可能會導致丟幀。前面提到每幀的渲染應該在16ms內完成,但在動畫過程中,由于已經被占用了不少時間,所以JavaScript代碼運行耗時應該控制在3-4毫秒。如果真的有特別耗時且不操作DOM元素的純計算工作,可以考慮放到Web Workers中執行。

          var dataSortWorker = new Worker("sort-worker.js");

          dataSortWorker.postMesssage(dataToSort);

          // 主線程不受Web Workers線程干擾

          dataSortWorker.addEventListener('message', function(evt) {

          var sortedData = e.data;

          // Web Workers線程執行結束

          // ...

          });

          3. 拆分操作DOM元素的任務,分別在多個frame完成

          由于Web Workers不能操作DOM元素的限制,所以只能做一些純計算的工作,對于很多需要操作DOM元素的邏輯,可以考慮分步處理,把任務分為若干個小任務,每個任務都放到 requestAnimationFrame中回調執行。

          var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);

          requestAnimationFrame(processTaskList);

          function processTaskList(taskStartTime) {

          var nextTask = taskList.pop;

          // 執行小任務

          processTask(nextTask);

          if (taskList.length > 0) {

          requestAnimationFrame(processTaskList);

          }

          }

          4. 使用Chrome DevTools的Timeline來分析JavaScript的性能

          打開 ChromeDevTools>Timeline>JSProfile,錄制一次動作,然后分析得到的細節信息,從而發現問題并修復問題。

          4.2 降低樣式計算的范圍和復雜度

          添加或移除一個DOM元素、修改元素屬性和樣式類、應用動畫效果等操作,都會引起DOM結構的改變,從而導致瀏覽器要repaint或者reflow。那么這里可以采取一些措施。

          1. 降低樣式選擇器的復雜度

          盡量保持class的簡短,或者使用Web Components框架。

          .box:nth-last-child(-n+1) .title {}

          // 改善后

          .final-box-title {}

          2. 減少需要執行樣式計算的元素個數

          由于瀏覽器的優化,現代瀏覽器的樣式計算直接對目標元素執行,而不是對整個頁面執行,所以我們應該盡可能減少需要執行樣式計算的元素的個數。

          4.3 避免大規模、復雜的布局

          布局就是計算DOM元素的大小和位置的過程,如果你的頁面中包含很多元素,那么計算這些元素的位置將耗費很長時間。布局的主要消耗在于:1. 需要布局的DOM元素的數量;2. 布局過程的復雜程度

          1. 盡可能避免觸發布局

          當你修改了元素的屬性之后,瀏覽器將會檢查為了使這個修改生效是否需要重新計算布局以及更新渲染樹,對于DOM元素的幾何屬性修改,比如width/height/left/top等,都需要重新計算布局。對于不能避免的布局,可以使用Chrome DevTools工具的Timeline查看布局的耗時,以及受影響的DOM元素數量。

          2. 使用flexbox替代老的布局模型

          老的布局模型以相對/絕對/浮動的方式將元素定位到屏幕上,而Floxbox布局模型用流式布局的方式將元素定位到屏幕上。通過一個小實驗可以看出兩種布局模型的性能差距,同樣對1300個元素布局,浮動布局耗時14.3ms,Flexbox布局耗時3.5ms。IE10+支持。

          3. 避免強制同步布局事件的發生

          根據渲染流程,JS腳本是在layout之前執行,但是我們可以強制瀏覽器在執行JS腳本之前先執行布局過程,這就是所謂的強制同步布局。

          requestAnimationFrame(logBoxHeight);

          // 先寫后讀,觸發強制布局

          function logBoxHeight {

          // 更新box樣式

          box.classList.add('super-big');

          // 為了返回box的offersetHeight值

          // 瀏覽器必須先應用屬性修改,接著執行布局過程

          console.log(box.offsetHeight);

          }

          // 先讀后寫,避免強制布局

          function logBoxHeight {

          // 獲取box.offsetHeight

          console.log(box.offsetHeight);

          // 更新box樣式

          box.classList.add('super-big');

          }

          在JS腳本運行的時候,它能獲取到的元素樣式屬性值都是上一幀畫面的,都是舊的值。因此,如果你在當前幀獲取屬性之前又對元素節點有改動,那就會導致瀏覽器必須先應用屬性修改,結果執行布局過程,最后再執行JS邏輯。

          4. 避免連續的強制同步布局發生

          如果連續快速的多次觸發強制同步布局,那么結果更糟糕。比如下面的例子,獲取box的屬性,設置到paragraphs上,由于每次設置paragraphs都會觸發樣式計算和布局過程,而下一次獲取box的屬性必須等到上一步設置結束之后才能觸發。

          function resizeWidth {

          // 會讓瀏覽器陷入'讀寫讀寫'循環

          for (var i = 0; i < paragraphs.length; i++)

          paragraphs[i].style.width = box.offsetWidth + 'px';

          }

          }

          // 改善后方案

          var width = box.offsetWidth;

          function resizeWidth {

          for (var i = 0; i < paragraphs.length; i++)

          paragraphs[i].style.width = width +px';

          }

          }

          注意:可以使用FastDOM來確保讀寫操作的安全,從而幫你自動完成讀寫操作的批處理,還能避免意外地觸發強制同步布局或快速連續布局,消除大量操作DOM的時候的布局抖動。

          4.4 簡化繪制的復雜度、減少繪制區域

          Paint就是填充像素的過程,通常這個過程是整個渲染流程中耗時最長的一環,因此也是最需要避免發生的一環。如果Layout被觸發,那么接下來元素的Paint一定會被觸發。當然純粹改變元素的非幾何屬性,也可能會觸發Paint,比如背景、文字顏色、陰影效果等。

          1. 提升移動或漸變元素的繪制層

          繪制并非總是在內存中的單層畫面里完成的,實際上,瀏覽器在必要時會將一幀畫面繪制成多層畫面,然后將這若干層畫面合并成一張圖片顯示到屏幕上。這種繪制方式的好處是,使用transform來實現移動效果的元素將會被正常繪制,同時不會觸發其他元素的繪制。

          2. 減少繪制區域,簡化繪制的復雜度

          瀏覽器會把相鄰區域的渲染任務合并在一起進行,所以需要對動畫效果進行精密設計,以保證各自的繪制區域不會有太多重疊。另外可以實現同樣效果的不同方式,應該采用性能更好的那種。

          3. 通過Chrome DevTools來分析繪制復雜度和時間消耗,盡可能降低這些指標

          打開DevTools,在彈出的面板中,選中 MoreTools>Rendering選項卡下的Paint flashing,這樣每當頁面發生繪制的時候,屏幕就會閃現綠色的方框。通過該工具可以檢查Paint發生的區域和時機是不是可以被優化。通過Chrome DevTools中的 Timeline>Paint選項可以查看更細節的Paint信息

          4.5 優先使用渲染層合并屬性、控制層數量

          1. 使用transform/opacity實現動畫效果

          使用transform/opacity實現動畫效果,會跳過渲染流程的布局和繪制環節,只做渲染層的合并。

          TypeFunc

          Position

          transform: translate(-px,-px)

          Scale

          transform: scale(-)

          Rotation

          transform: rotate(-deg)

          Skew

          transform: skew(X/Y)(-deg)

          Matrix

          transform: matrix(3d)(..)

          Opacity

          opacity: 0-1

          使用transform/opacity的元素必須獨占一個渲染層,所以必須提升該元素到單獨的渲染層。

          2. 提升動畫效果中的元素

          應用動畫效果的元素應該被提升到其自有的渲染層,但不要濫用。在頁面中創建一個新的渲染層最好的方式就是使用CSS屬性will-change,對于目前還不支持will-change屬性、但支持創建渲染層的瀏覽器,可以通過3D transform屬性來強制瀏覽器創建一個新的渲染層。需要注意的是,不要創建過多的渲染層,這意味著新的內存分配和更復雜的層管理。注意,IE11,Edge17都不支持這一屬性。

          .moving-element {

          will-change: transform;

          transform: translateZ(0);

          }

          3. 管理渲染層、避免過多數量的層

          盡管提升渲染層看起來很誘人,但不能濫用,因為更多的渲染層意味著更多的額外的內存和管理資源,所以當且僅當需要的時候才為元素創建渲染層。

          * {

          will-change: transform;

          transform: translateZ(0);

          }

          4. 使用Chrome DevTools來了解頁面的渲染層情況

          開啟 Timeline>Paint選項,然后錄制一段時間的操作,選擇單獨的幀,看到每個幀的渲染細節,在ESC彈出框有個Layers選項,可以看到渲染層的細節,有多少渲染層,為何被創建?

          4.6 對用戶輸入事件的處理函數去抖動(移動設備)

          用戶輸入事件處理函數會在運行時阻塞幀的渲染,并且會導致額外的布局發生。

          1. 避免使用運行時間過長的輸入事件處理函數

          理想情況下,當用戶和頁面交互,頁面的渲染層合并線程將接收到這個事件并移動元素。這個響應過程是不需要主線程參與,不會導致JavaScript、布局和繪制過程發生。但是如果被觸摸的元素綁定了輸入事件處理函數,比如touchstart/touchmove/touchend,那么渲染層合并線程必須等待這些被綁定的處理函數執行完畢才能執行,也就是用戶的滾動頁面操作被阻塞了,表現出的行為就是滾動出現延遲或者卡頓。

          簡而言之就是你必須確保用戶輸入事件綁定的任何處理函數都能夠快速的執行完畢,以便騰出時間來讓渲染層合并線程完成他的工作。

          2. 避免在輸入事件處理函數中修改樣式屬性

          輸入事件處理函數,比如scroll/touch事件的處理,都會在requestAnimationFrame之前被調用執行。因此,如果你在上述輸入事件的處理函數中做了修改樣式屬性的操作,那么這些操作就會被瀏覽器暫存起來,然后在調用requestAnimationFrame的時候,如果你在一開始就做了讀取樣式屬性的操作,那么將會觸發瀏覽器的強制同步布局操作。

          3. 對滾動事件處理函數去抖動

          通過requestAnimationFrame可以對樣式修改操作去抖動,同時也可以使你的事件處理函數變得更輕。

          function onScroll(evt) {

          // Store the scroll value for laterz.

          lastScrollY = window.scrollY;

          // Prevent multiple rAF callbacks.

          if (scheduledAnimationFrame) {

          return;

          }

          scheduledAnimationFrame = true;

          requestAnimationFrame(readAndUpdatePage);

          }

          window.addEventListener('scroll', onScroll);

          作者簡介:SHERlocked93,來自南京的前端程序員,本碩畢業于北京理工大學,熱愛分享,個人公眾號「前端下午茶」,期待在這里和大家共同進步 ~


          主站蜘蛛池模板: 色系一区二区三区四区五区| 无码人妻少妇色欲AV一区二区| 久久一区二区三区精华液使用方法 | 免费看一区二区三区四区| 国产肥熟女视频一区二区三区| 日韩一区二区三区在线| 无码aⅴ精品一区二区三区| 精品无码国产一区二区三区麻豆| 国产一区二区三区不卡在线观看 | 97se色综合一区二区二区| 亚洲色精品三区二区一区| 制服美女视频一区| 亚洲国产精品一区二区成人片国内| 人妻av无码一区二区三区| 无码午夜人妻一区二区不卡视频| 日韩精品人妻一区二区三区四区 | 精品国产亚洲一区二区三区| 蜜桃视频一区二区三区在线观看| 日本一区二区视频| 一区二区中文字幕在线观看| 日韩精品区一区二区三VR| 国产精品盗摄一区二区在线| 中文日韩字幕一区在线观看| 精品一区二区三区东京热| 日本无卡码免费一区二区三区| 精品视频一区二区三区| 日本一区频道在线视频| 国产一区二区三区高清视频| 中文乱码人妻系列一区二区| 一区在线免费观看| 亚洲日韩中文字幕无码一区| 国产乱码精品一区二区三区中文| 欧美一区内射最近更新| 无码人妻一区二区三区免费看| 无码人妻精品一区二区蜜桃百度| 精品无码人妻一区二区三区品| 无码日韩人妻AV一区二区三区| 日本激情一区二区三区| 99久久精品国产高清一区二区| 中文字幕日韩欧美一区二区三区 | 无码日韩AV一区二区三区|