天天看點

浏覽器層面優化前端性能(2):Reader引擎線程與子產品分析優化點

作者:周陸軍

Reader 引擎線程與子產品分析

浏覽器層面優化前端性能(2):Reader引擎線程與子產品分析優化點
浏覽器層面優化前端性能(2):Reader引擎線程與子產品分析優化點
首先是網頁内容,加載完輸入到HTML解釋器,解釋後構成DOM樹,這期間如果遇到JavaScript代碼就交給JavaScript引擎去處理,如果網頁中包含CSS,就交給CSS解釋器;DOM樹履歷的時候,渲染引擎接收來自CSS解釋器的樣式資訊,建構一個新的你日不會吐模型,該模型由布局子產品計算模型内部各個元素的位置和大小資訊

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

  1. 解析HTML生成DOM樹 - 渲染引擎首先解析HTML文檔,生成DOM樹
  2. 建構Render樹 - 接下來不管是内聯式,外聯式還是嵌入式引入的CSS樣式會被解析生成CSSOM樹,根據DOM樹與CSSOM樹生成另外一棵用于渲染的樹-渲染樹(Render tree),
  3. 布局Render樹 - 然後對渲染樹的每個節點進行布局處理,确定其在螢幕上的顯示位置
  4. 繪制Render樹 - 最後周遊渲染樹并用UI後端層将每一個節點繪制出來

DOM樹與Render樹

浏覽器層面優化前端性能(2):Reader引擎線程與子產品分析優化點

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

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

布局與繪制

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

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

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

在繪制(painting)階段,渲染引擎會周遊Render樹,并調用renderer的 paint() 方法,将renderer的内容顯示在螢幕上。繪制工作是使用UI後端元件完成的。

回流與重繪

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

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

關鍵渲染路徑與阻塞渲染

在浏覽器拿到HTML、CSS、JS等外部資源到渲染出頁面的過程,有一個重要的概念關鍵渲染路徑(Critical Rendering Path)。

例如為了保障首屏内容的最快速顯示,通常會提到一個漸進式頁面渲染,但是為了漸進式頁面渲染,就需要做資源的拆分,那麼以什麼粒度拆分、要不要拆分,不同頁面、不同場景政策不同。

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

  • CSS 被視為渲染阻塞資源(包括JS),這意味着浏覽器将不會渲染任何已處理的内容,直至 CSSOM 建構完畢,才會進行下一階段。
  • 存在阻塞的 CSS 資源時,浏覽器會延遲 JavaScript 的執行和 DOM 建構
    • css加載不會阻塞DOM樹的解析
    • css加載會阻塞DOM樹的渲染
    • css不會阻塞JS的加載
    • css加載會阻塞後面js語句的執行
  • JavaScript 被認為是解釋器阻塞資源,HTML解析會被JS阻塞,它不僅可以讀取和修改 DOM 屬性,還可以讀取和修改 CSSOM 屬性。
    • 當浏覽器遇到一個 script 标記時,DOM 建構将暫停,直至腳本完成執行。
    • JavaScript 可以查詢和修改 DOM 與 CSSOM。
    • CSSOM 建構時,JavaScript 執行将暫停,直至 CSSOM 就緒。
    • 沒有js的理想情況下,html與css會并行解析,分别生成DOM與CSSOM,然後合并成Render Tree,進入Rendering Pipeline;但如果有js,css加載會阻塞後面js語句的執行,而(同步)js腳本執行會阻塞其後的DOM解析(是以通常會把css放在頭部,js放在body尾)

CSS 優先:引入順序上,CSS 資源先于 JavaScript 資源。JavaScript 應盡量少影響 DOM 的建構。

改變腳本加載次序defer/async/document.createElement

defer

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 預設是異步的

通過動态添加 script 标簽引入 JavaScript 檔案預設是不會阻塞頁面的。如果想同步執行,需要将 async 屬性人為設定為 false。

優化渲染性能

chrome 官方文檔:https://developers.google.com/web/fundamentals/performance/?hl=en

翻譯:https://x5.tencent.com/tbs/document/doc-chrome.html

優化JS的執行效率

動畫實作使用requestAnimationFrame

setTimeout(callback)和setInterval(callback)無法保證callback函數的執行時機,很可能在幀結束的時候執行,進而導緻丢幀。

requestAnimationFrame(callback)可以保證callback函數在每幀動畫開始的時候執行。拓展閱讀《頻率史—從電源頻率到音頻采樣頻率與視訊幀率:29.97/44.1》、《弄懂javascript的執行機制:事件輪詢|微任務和宏任務》

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

JS代碼運作在浏覽器的主線程上,與此同時,浏覽器的主線程還負責樣式計算、布局、繪制的工作,如果JavaScript代碼運作時間過長,就會阻塞其他渲染工作,很可能會導緻丢幀。

前面提到每幀的渲染應該在16ms内完成,但在動畫過程中,由于已經被占用了不少時間,是以JavaScript代碼運作耗時應該控制在3-4毫秒。

如果真的有特别耗時且不操作DOM元素的純計算工作,可以考慮放到Web Workers中執行。

CSS渲染與布局優化

添加或移除一個DOM元素、修改元素屬性和樣式類、應用動畫效果等操作,都會引起DOM結構的改變,進而導緻浏覽器要repaint或者reflow。

降低樣式選擇器的複雜度

  • 盡量保持class的簡短,或者使用Web Components架構(如:Omi)。
  • 降低樣式選擇器的複雜度;使用基于class的方式,比如BEM(Block, Element, Modifier)。
  • 減少css嵌套,如sass使用@at-root
  • 減少需要執行樣式計算的元素的個數
  • 對于樣式計算來說,範圍越小、規則越簡單的話,處理效率越高。
    • 在過去,如果你修改了body元素的class屬性,那麼頁面裡所有元素都要重新計算樣式。現代的浏覽器中不再這樣做了,浏覽器不會檢查所有受到樣式變化影響的元素。因為會對每個DOM元素維護一個獨有的樣式規則小集合,如果這個集合發生改變,才重新計算該元素的樣式。是以,樣式計算一般是直接對那些目标元素執行。是以我們應該盡可能減少需要執行樣式計算的元素的個數。
    • 一般來說在最壞的情況下,樣式計算量 = 元素個數 x 樣式選擇器個數。因為對每個元素最少需要檢查一次所有的樣式,以确認是否
    • Web Components中的樣式計算不會跨越Shadow DOM範圍,僅在單個的Web Component中進行,而不是在整個頁面的DOM樹上進行

避免大規模、複雜的布局

布局,就是浏覽器計算DOM元素的幾何資訊的過程:元素大小和在頁面中的位置。每個元素都有一個顯式或隐式的大小資訊,決定于其CSS屬性的設定、或是元素本身内容的大小、抑或是其父元素的大小。在Blink/WebKit核心的浏覽器和IE中,這個過程稱為布局。在基于Gecko的浏覽器(比如Firefox)中,這個過程稱為Reflow。

盡可能避免觸釋出局

布局的時間消耗主要在于:

  • 需要布局的DOM元素的數量
  • 布局過程的複雜程度

一份詳細的能觸釋出局、繪制或渲染層合并的CSS屬性清單:CSS Triggers

使用flexbox替代老的布局模型

新的Flexbox比舊的Flexbox和基于浮動的布局模型更高效。

在任何情況下,不管是是否使用Flexbox,你都應該努力避免同時觸發所有布局,特别在頁面對性能敏感的時候(比如執行動畫效果或頁面滾動時)。

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

将一幀畫面渲染到螢幕上的處理順序如下所示:

浏覽器層面優化前端性能(2):Reader引擎線程與子產品分析優化點
  • 在JavaScript腳本運作的時候,它能擷取到的元素樣式屬性值都是上一幀畫面的,都是舊的值。
  • 如果想在這一幀開始的時候,讀取一個元素屬性值,就需要修改目前元素的某個屬性(可能觸發重繪與回流)。
  • 為了避免觸發不必要的布局過程,你應該首先批量讀取元素樣式屬性,然後再對樣式屬性進行寫操作。

大多數情況下,都不需要先修改然後再讀取元素的樣式屬性值,使用上一幀的值就足夠了。過早地同步執行樣式計算和布局是潛在的頁面性能的瓶頸之一

避免快速連續的布局

比強制同步布局更糟:連續快速的多次執行它。如:

for (var i = 0; i < paragraphs.length; i++) {
     paragraphs[i].style.width = box.offsetWidth + 'px';
}           

FastDom是一個輕量的庫,它提供一個公共接口,能讓DOM的讀/寫操作捆綁在一起。

https://github.com/wilsonpage/fastdom

簡化繪制的複雜度、減小繪制區域

繪制并非總是在記憶體中的單層畫面裡完成的。實際上,浏覽器在必要時将會把一幀畫面繪制成多層畫面,然後将這若幹層畫面合并成一張圖檔顯示到螢幕上。

這種繪制方式的好處是,使用tranforms來實作移動效果的元素将會被正常繪制,同時不會觸發對其他元素的繪制。這種處理方式和思想跟圖像處理軟體(比如Sketch/GIMP/Photoshop)是一緻的,它們都是可以在圖像中的某個單個圖層上做操作,最後合并所有圖層得到最終的圖像。

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

  • 在頁面中建立一個新的渲染層的最好方式就是使用CSS屬性will-change,同時再與transform屬性一起使用,就會建立一個新的組合層:will-change: transform;
  • 對于那些目前還不支援will-change屬性、但支援建立渲染層的浏覽器,以使用一個3D transform屬性來強制浏覽器建立一個新的渲染層:transform: translateZ(0);

減少繪制區域

有時候盡管把元素提升到了一個單獨的渲染層,渲染工作依然是必須的。渲染過程中一個比較有挑戰的問題是,浏覽器會把兩個相鄰區域的渲染任務合并在一起進行,這将導緻整個螢幕區域都會被繪制。比如,你的頁面頂部有一個固定位置的header,而此時螢幕底部有某個區域正在發生繪制的話,整個螢幕都将會被繪制。

注意:在DPI較高的螢幕上,固定定位的元素會自動地被提升到一個它自有的渲染層中。但在DPI較低的裝置上卻并非如此,因為這個渲染層的提升會使得字型渲染方式由子像素變為灰階(詳細内容請參考:Text Rendering),我們需要手動實作渲染層的提升。

減少繪制區域通常需要對動畫效果進行精密設計,以保證各自的繪制區域之間不會有太多重疊,或者想辦法避免對頁面中某些區域執行動畫效果。

簡化繪制的複雜度

比如js 擷取元素的offsetTop ffsetTop 比如getBoundingClientRect 消耗更少。

在css裡面,重繪 backgroun 比如 box-shadow 消耗更好。

那些能性能更加耗資源,我也不知道,道友若知,請留言賜教,多謝。手工就 paint profiler 分析對比咯

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

  • 隻使用transform/opacity來實作動畫效果
  • 應用了transforms/opacity屬性的元素必須獨占一個渲染層。為了對這個元素建立一個自有的渲染層,你必須提升該元素。在合成層上面的元素,也會合并到此圖層中。
  • 用will-change/translateZ屬性把動畫元素提升到單獨的渲染層中
  • 避免濫用渲染層提升:更多的渲染層需要更多的記憶體和更複雜的管理
  • 過多的渲染層來帶的開銷而對頁面渲染性能産生的影響,甚至遠遠超過了它在性能改善上帶來的好處。由于每個渲染層的紋理都需要上傳到GPU處理,是以我們還需要考慮CPU和GPU之間的帶寬問題、以及有多大記憶體供GPU處理這些紋理的問題。

從性能方面考慮,最理想的渲染流水線是沒有布局和繪制環節的,隻需要做渲染層的合并即可:

之前也參看:《關于css3之transform一些坑的總結-transform對普通元素的N多渲染》

對使用者輸入事件的處理去抖動

  • 避免使用運作時間過長的輸入事件處理函數,它們會阻塞頁面的滾動
  • 避免在輸入事件處理函數中修改樣式屬性
  • 對輸入事件處理函數去抖動,存儲事件對象的值,然後在requestAnimationFrame 回調函數中修改樣式屬性

具體參看《Debounce 和 Throttle 的原理及實作》

參考文章:

從浏覽器多程序到JS單線程,JS運作機制最全面的一次梳理 https://www.cnblogs.com/cangqinglang/p/8963557.html

Chrome源碼剖析、上--多線程模型、程序通信、程序模型https://www.cnblogs.com/v-July-v/archive/2011/04/02/2036008.html

Chrome源代碼分析之程序和線程模型(三) https://blog.csdn.net/namelcx/article/details/6582730

http://dev.chromium.org/developers/design-documents/multi-process-architecture

chrome渲染機制淺析 https://www.jianshu.com/p/99e450fc04a5

淺析浏覽器渲染原理 https://segmentfault.com/a/1190000012960187

javascript宏任務和微任務 https://www.cnblogs.com/fangdongdemao/p/10262209.html

浏覽器與Node的事件循環(Event Loop)有何差別? https://blog.csdn.net/Fundebug/article/details/86487117

轉載本站文章《浏覽器層面優化前端性能(2):Reader引擎線程與子產品分析優化點》,

請注明出處:https://www.zhoulujun.cn/html/webfront/browser/webkit/2020_0615_8464.html

繼續閱讀