瀏覽器原理

lhyt發表於2018-05-27

本文來自於我的github

0. 前言

身為前端,打交道最多的就是瀏覽器和node了,也是我們必須熟悉的。接下來我們講一下瀏覽器工作原理和工作過程。從url到頁面的過程,......,我們直接來到收到伺服器返回內容部分開始。

先上很多人都見過的一幅圖:

image

還有一幅圖:

image

瀏覽器主要組成部分:

  • 瀏覽器引擎:在使用者介面和呈現引擎之間傳送指令。
  • 渲染引擎:負責顯示請求的內容。如果請求的內容是 HTML,它就負責解析 HTML 和 CSS 內容,並將解析後的內容顯示在螢幕上。
  • 網路:用於網路呼叫,比如 HTTP 請求。其介面與平臺無關,併為所有平臺提供底層實現。
  • JavaScript 直譯器:用於解析和執行 JavaScript 程式碼。
  • 資料儲存:瀏覽器需要在硬碟上儲存各種資料,例如 Cookie、storage、indexdb。

過程:(重要)

  1. 解析過程
  2. CSS樣式計算
  3. 構建Render Tree
  4. layout:佈局。定位座標和大小,是否換行,position, overflow之類的屬性。確定了每個DOM元素的樣式規則後,計算每個DOM元素最終在螢幕上顯示的大小和位置。Web頁面中元素的佈局是相對的,因此一個元素的佈局發生變化,會聯動地引發其他元素的佈局發生變化。比如body元素的width變化會影響其後代元素的寬度。因此,佈局過程是經常發生的。
  5. paint:繪製文字、顏色、影象、邊框和陰影等,也就是一個DOM元素所有的可視效果。一般來說,這個繪製過程是在多個層上完成的。
  6. composite:渲染層合併。頁面中DOM元素的繪製是在多個層上進行的,在每個層上完成繪製過程之後,瀏覽器會將所有層按照合理的順序合併成一個圖層,然後在螢幕上呈現。
    image

1. 解析過程

  • 獲取請求文件的內容後,呈現引擎將開始解析 HTML 文件,並將各標記逐個轉化成“內容樹”上的 DOM 節點。
  • 解析外部 CSS以及style元素中的樣式資料形成呈現樹。呈現樹包含多個帶有視覺屬性(如顏色和尺寸)的矩形。這些矩形的排列順序就是它們將在螢幕上顯示的順序。呈現樹構建完畢之後,進入“佈局”處理階段,也就是為每個節點分配一個應出現在螢幕上的確切座標。
  • 解析script標籤時,解析完畢馬上執行,並且阻塞頁面。
  • 繪製 - 呈現引擎會遍歷呈現樹,由使用者介面後端層將每個節點繪製出來。

1.1 詞法、語法分析與編譯

詞法分析器將輸入內容分解成一個個有效標記,解析器負責根據語言的語法規則分析文件的結構來構建解析樹。詞法分析器知道如何將無關的字元(空格、換行符等)分離出來,所以我們平時寫一些空格也不會影響大局。

在語法分析的過程中,解析器會向詞法分析器請求一個標記(就是前面分解出來的標記),並嘗試將其與某條語法規則(比如標籤要閉合、正確巢狀)進行匹配。如果發現了匹配規則,解析器會將一個對應於該標記的節點新增到解析樹中,然後繼續請求下一個標記。

如果沒有規則可以匹配,解析器就會將標記儲存到內部,並繼續請求標記,直至找到可與所有內部儲存的標記匹配的規則(如div多層巢狀的情況,這樣子能找到div閉合部分)。如果找不到任何匹配規則,解析器就會引發一個異常。這意味著文件無效,包含語法錯誤。

解析器型別有兩種:

  • 自上而下解析器:從語法的高層結構出發,嘗試從中找到匹配的結構。
  • 自下而上解析器:從低層規則出發,將輸入內容逐步轉化為語法規則,直至滿足高層規則。將掃描輸入內容,找到匹配的規則後,將匹配的輸入內容替換成規則。如此繼續替換,直到輸入內容的結尾。部分匹配的表示式儲存在解析器的堆疊中。

編譯:將原始碼編譯成機器程式碼,原始碼先走完解析的過程形成成解析樹,解析樹被翻譯成機器程式碼文件,完成編譯的過程

1.2 DTD

特殊的是,恰好html不能用上面兩種解析方法。有一種可以定義 HTML 的正規格式:DTD,但它不是與上下文無關的語法,html明顯是和上下文關係緊密的。我們知道 HTML 是有點“隨意”的,對於不閉合的或者不正確巢狀標籤有可能不報錯,並且嘗試解釋成正確的樣子,具有一定的容錯機性,因此可以達到簡化網路開發的效果。另一方面,這使得它很難編寫正式的語法。概括地說,HTML 無法很容易地通過常規解析器解析(因為它的語法不是與上下文無關的語法),所以採用了 DTD 格式。

1.3 解析為dom過程

解析器解析html文件的解析樹是由 DOM 元素和屬性節點構成的樹結構。它是 HTML 文件的物件表示,同時也是外部內容(例如 JavaScript)與 HTML 元素之間的api,其根節點是document。上面已經說到,不能使用常規的解析技術解釋html,瀏覽器就建立了自定義的解析器來解析 。對於HTML/SVG/XHTML這三種文件,Webkit有三個C++的類對應這三種文件,併產生一個DOM Tree。解釋html成dom的過程,由兩個階段組成:標記化和樹構建。

1.3.1 標記化演算法

對於一段html:

<html>
<body>
hi
</body>
</html>
複製程式碼

該演算法使用狀態機來表示。每一個狀態接收來自輸入資訊流的一個或多個字元,並根據這些字元更新下一個狀態。當前的標記化狀態和樹結構狀態會影響進入下一狀態的決定。

初始狀態是資料狀態。遇到字元 < 時,狀態更改為“標記開啟狀態”。接收一個字母會建立“起始標記”,狀態更改為“標記名稱狀態”。這個狀態會一直保持到接收 > 字元,接收到將會進入“標記開啟狀態”。在此期間接收的每個字元都會附加到新的標記名稱上。

  1. 比如我們先寫html標籤,先遇到<,進入“標記開啟狀態”,遇到html四個字母進入“標記名稱狀態”,接著接收到了>字元,會傳送當前的標記,狀態改回“資料狀態”

  2. <body> 標記也會進行同樣的處理。現在 html 和 body 標記均已發出,而且目前是“資料狀態”。接收到 hi中的 h 字元時,將建立併傳送字元標記,直到接收 </body> 中的 <。我們將為hi的每個字元都傳送一個字元標記。

  3. 回到“標記開啟狀態”。接收下一個輸入字元 / 時,會建立閉合標籤token,並改為“標記名稱狀態”。我們會再次保持這個狀態,直到接收 >。然後將傳送新的標記,並回到“資料狀態”。最後,</html> 輸入也會進行同樣的處理。

1.3.2 樹構建過程

在建立解析器的同時也會建立 document 物件。在樹構建階段,以 Document 為根節點的 DOM 樹也會不斷進行修改,向其中新增各種元素。標記生成器傳送的每個節點都會由樹構建器進行處理。

  1. 樹構建階段的輸入是一個來自標記化階段的標記序列。第一個模式是“initial mode”。接收 HTML 標記後轉為“before html”模式,並在這個模式下重新處理此標記。這樣會建立一個 HTMLHtmlElement 元素,並將其附加到 Document 根物件上。

  2. 狀態改為“before head”。此時我們接收“body”標記。由於容錯性,就算我們的沒head標籤,系統也會隱式建立一個 HTMLHeadElement,並將其新增到樹中。

  3. 進入了“in head”模式,然後轉入“after head”模式。系統對 body 標記進行重新處理,建立並插入 HTMLBodyElement,同時模式轉變為“in body”。

  4. 接收由“hi”字串生成的一系列字元標記。接收第一個字元時會建立並插入文字節點,而其他字元也將附加到該節點。當然還有其他節點,比如屬性節點、換行節點。我們實際場景還有外部資源以及其他各種各樣的複雜標籤巢狀和內容結構,不過原理都類似。對於中間這個過程,遇到外部資源如何處理,順序是怎樣的,後面再講。

  5. 接收 body 結束標記會觸發“after body”模式。現在我們將接收 HTML 結束標記,然後進入“after after body”模式。接收到檔案結束標記後,解析過程就此結束,dom樹已經建立完畢(不是載入完畢,在DOMContentLoaded之前,document.readyState = ‘interactive ’)。

結束後,此時文件被標註為互動狀態,瀏覽器開始解析那些script標籤上帶有“defer”指令碼,也就是那些應在文件解析完成後才執行的指令碼,文件狀態將設定為“完成”,執行完畢觸發DOMContentLoaded事件(當初始的 HTML 文件被完全載入和解析完成之後,DOMContentLoaded 事件被觸發,不會等待樣式表、影象和iframe的完成載入)。

1.4 css和js解析過程

1.4.1 css解析

解析CSS會產生CSS規則樹,前面已經說到,html不是與上下文無關的語法,而css和js是與上下文無關的語法,所以常規的解析方法都可以用。對於建立CSS 規則樹,是需要比照著DOM樹來的。CSS匹配DOM樹主要是從右到左解析CSS選擇器。解析CSS的順序是瀏覽器的樣式 -> 使用者自定義的樣式 -> 頁面的link標籤等引進來的樣式 -> 寫在style標籤裡面的內聯樣式

樣式表不會更改 DOM 樹,因此沒有必要等待樣式表並停止文件解析。而指令碼在文件解析階段會請求樣式資訊時還沒有載入和解析樣式,指令碼就會獲得錯誤的回覆。Firefox 在樣式表載入和解析的過程中,會禁止所有指令碼。而對於 WebKit 而言,僅當指令碼嘗試訪問的樣式屬性可能受尚未載入的樣式表影響時,它才會禁止該指令碼。

1.4.2 js解析(重要)

  • 網路整個解析的過程是同步的,會暫停 DOM 的解析。解析器遇到 script標記時立即解析並執行指令碼。文件的解析將停止,直到指令碼執行完畢。

  • 如果指令碼是外部的,那麼解析過程會停止,直到從網路同步抓取資源完成後再繼續。

  • 目前瀏覽器的script標籤是並行下載的,他們互相之間不會阻塞,但是會阻塞其他資源(圖片)的下載

所以為了使用者體驗,後來有了async和defer,將指令碼標記為非同步,不會阻塞其他執行緒解析和執行。標註為“defer”的script不會停止文件解析,而是等到解析結束才執行;標註為“async”只能引用外部指令碼,下載完馬上執行,而且不能保證載入順序。

image

指令碼的預解析:在執行指令碼時,其他執行緒會解析文件的其餘部分,找出並載入需要通過網路載入的其他資源。通過這種方式,資源可以在並行連線上載入,從而提高總體速度。請注意,預解析器不會修改 DOM 樹,而是將這項工作交由主解析器處理;預解析器只會解析外部資源(例如外部指令碼、樣式表和圖片)的引用。

指令碼主要是通過DOM API和CSSOM API來操作DOM Tree和CSS Rule Tree.

另外,我們又可以想到一個問題,為什麼jsonp能response一個類eval字串就馬上執行呢?其實也是因為普通的script標籤解析完成就馬上執行,我們在伺服器那邊大概是這樣子返回: res.end('callback('+data+')')

整個過程,就是:動態建立script標籤,src為伺服器的一個get請求介面,遇到src當然馬上請求伺服器,然後伺服器返回處理data的callback函式這樣子的程式碼。其實,我們可以看作是前端發get請求,服務端響應文件是js檔案,而且這個檔案只有一行程式碼:callback(data)。當然你可以寫很多程式碼,不過一般沒見過有人這麼幹。

2. 渲染樹

html、css、js解析完成後,瀏覽器引擎會通過DOM Tree 和 CSS Rule Tree 來構造 Rendering Tree(渲染樹)。

  1. 在渲染樹中,會把DOM樹中沒有的元素給去除,比如head標籤以及裡面的內容,以及display:none的元素也會被去除,但是 visibility 屬性值為“hidden”的元素仍會顯示
  2. CSS 的 Rule Tree主要是為了完成匹配並把CSS Rule附加上渲染樹上的每個Element,也就是所謂的Frame(Firefox 將渲染樹中的元素稱為frame,WebKit 的是呈現器或呈現物件,其實就是DOM節點,別以為是什麼高大上的東西。 呈現器知道如何佈局並將自身及其子元素繪製出來 )。然後,計算每個Frame的位置,這通常是layout和reflow過程中發生。
  3. 一旦渲染樹構建完成,瀏覽器會把樹裡面的內容繪製在螢幕上。

需要注意的點:

  • 有一些 DOM 元素對應多個視覺化物件。它們往往是具有複雜結構的元素,無法用單一的矩形來描述。如“select”元素有 3 個呈現器:一個用於顯示區域,一個用於下拉選單框,還有一個用於按鈕。如果由於寬度不夠,文字無法在一行中顯示而分為多行,那麼新的行也會作為新的呈現器而新增。

  • inline 元素只能包含 block 元素或 inline 元素中的一種。如果出現了混合內容,則應建立匿名的 block 呈現器,以包裹 inline 元素。所以我們平時的inline-block可以設定寬高。

  • 有一些呈現物件對應於 DOM 節點,但在樹中所在的位置與 DOM 節點不同。脫離文件流的浮動定位和絕對定位的元素就是這樣,被放置在樹中的其他地方,並對映到真正的frame,而放在原位的是佔位frame。

2.1 CSS樣式計算

構建渲染樹之前,需要計算每一個呈現物件的視覺化屬性。這是通過計算每個元素的樣式屬性來完成的。

Firefox:CSS 解析生成 CSS Rule Tree,通過比對DOM生成Style Context Tree,然後Firefox通過把Style Context Tree和其Render Tree(Frame Tree)關聯上完成樣式計算

Webkit:把Style物件直接存在了相應的DOM結點上了

樣式被js改變過的話,會重新計算樣式(Recalculate Style)。Recalculate被觸發的時,處理指令碼給元素設定的樣式。Recalculate Style會計算Render樹(渲染樹),然後從根節點開始進行頁面渲染,將CSS附加到DOM上的過程。所以任何企圖改變元素樣式的操作都會觸發Recalculate,在JavaScript執行完成後才觸發的,下面將會講到的layout也是。

2.2 構建渲染樹

Firefox:系統會針對 DOM 更新註冊展示層,作為偵聽器。展示層將框架建立工作委託FrameConstructor,由該構造器解析樣式並建立frame。

WebKit:解析樣式和建立呈現器的過程稱為“附加”。每個 DOM 節點都有一個“attach”方法。附加是同步進行的,將節點插入 DOM 樹需要呼叫新的節點“attach”方法。

處理 html 和 body 標記就會構建渲染樹根節點。這個根節點呈現物件對應於 CSS 規範中所說的容器 block,這是最上層的 block,包含了其他所有 block。它的尺寸就是視口,即瀏覽器視窗顯示區域的尺寸。Firefox 稱之為 ViewPortFrame,而 WebKit 稱之為 RenderView。這就是文件所指向的呈現物件。渲染樹的其餘部分以 DOM 樹節點插入的形式來構建。

3. 佈局(重要)

呈現器在建立完成並新增到渲染樹時,並不包含位置和大小資訊。**計算這些值的過程**稱為佈局(layout)或重排(repaint)。這個得記住了,記準確了!為什麼呢?計算offsetWidth和offsetHeight的、js操作dom、改變style屬性時候,都會引發重排!

前面通過樣式計算確定了每個DOM元素的樣式,這一步就是具體計算每個DOM元素最終在螢幕上顯示的大小和位置。Web頁面中元素的佈局是相對的,因此一個元素的佈局發生變化,會聯動地引發其他元素的佈局發生變化。比如,元素的width變化會影響其後代元素的寬度。因此,layout過程是經常發生的。

HTML 是流式佈局,這意味著大多數情況下只要一次遍歷就能計算出幾何資訊。處於流中靠後位置元素通常不會影響靠前位置元素的幾何特徵,因此佈局可以按從左至右、從上至下的順序遍歷文件。座標系是相對於根節點而建立的,使用的是上座標和左座標。根呈現器的位置左邊是 0,0,其尺寸為視口。layout過程計算一個元素絕對的位置和尺寸。Layout計算的是佈局位置資訊。任何有可能改變元素位置或大小的樣式都會觸發這個Layout事件。

layout是一個遞迴的過程。它從根呈現器(對應於 HTML 文件的 元素)開始,然後遞迴遍歷部分或所有的框架層次結構,為每一個需要計算的呈現器計算幾何資訊。所有的呈現器都有一個“layout”或者“reflow”方法,每一個呈現器都會呼叫其需要進行佈局的子代的 layout 方法。任何有可能改變元素位置或大小的樣式都會觸發這個Layout事件。

由於元素相覆蓋,相互影響,稍有不慎的操作就有可能導致一次自上而下的佈局計算。所以我們在進行元素操作的時候要一再小心儘量避免修改這些重新佈局的屬性。當你修改了元素的樣式(比如width、height或者position等)也就是修改了layout,那麼瀏覽器會檢查哪些元素需要重新佈局,然後對頁面激發一個reflow過程完成重新佈局。被reflow的元素,接下來也會激發繪製過程也就是重繪(repaint),最後激發渲染層合併過程,生成最後的畫面。由於元素相覆蓋,相互影響,稍有不慎的操作就有可能導致一次自上而下的佈局計算。所以我們在進行元素操作的時候要一再小心儘量避免修改這些重新佈局的屬性。

如果呈現器在佈局過程中需要換行,會立即停止佈局,並告知其父代需要換行。父代會建立額外的呈現器,並對其呼叫佈局。

幾種佈局模式

  1. 父呈現器確定自己的寬度。
  2. 父呈現器依次處理子呈現器,並且放置子呈現器(設定 x,y 座標)。如果有必要,呼叫子呈現器的佈局,這會計運算元呈現器的高度。
  3. 父呈現器根據子呈現器的累加高度以及邊距和補白的高度來設定自身高度,此值也可供父呈現器的父呈現器使用。

3.1 Dirty 位系統(Dirty bit system)

為避免對所有細小更改都進行整體佈局,瀏覽器採用了一種“dirty 位”系統。如果某個呈現器發生了更改,或者將自身及其子代標註為“dirty”,則需要進行佈局。類似於髒檢測。

有“dirty”和“children are dirty”兩種標記方法。“children are dirty”表示儘管呈現器自身沒有變化,但它至少有一個子代需要佈局。dirty就是自己都變化了。

3.2 全域性佈局和增量佈局

  • 全域性佈局:指觸發了整個呈現樹範圍的佈局,呈現器的全域性樣式更改或者螢幕大小調整都會觸發全域性佈局。
  • 增量佈局:採用增量方式,也就是隻對 dirty 呈現器進行佈局(這樣可能存在需要進行額外佈局的弊端)。

當呈現器為 dirty 時,會非同步觸發增量佈局。例如,當來自網路的額外內容新增到 DOM 樹之後,新的呈現器附加到了呈現樹中。

3.3 非同步佈局和同步佈局

增量佈局是非同步執行的。Firefox 將增量佈局的“reflow 命令”加入佇列,而排程程式會觸發這些命令的批量執行。WebKit 也有用於執行增量佈局的計時器:對呈現樹進行遍歷,並對 dirty 呈現器進行佈局。 請求樣式資訊(例如“offsetHeight”)的指令碼可同步觸發增量佈局。 全域性佈局往往是同步觸發的。 有時,當初始佈局完成之後,如果一些屬性(如滾動位置)發生變化,佈局就會作為回撥而觸發。

瀏覽器的自身優化

如果佈局是由“大小調整”或呈現器的位置(而非大小)改變而觸發的,那麼可以從快取中獲取呈現器的大小,而無需重新計算。 在某些情況下,只有一個子樹進行了修改,因此無需從根節點開始佈局。這適用於在本地進行更改而不影響周圍元素的情況,例如在文字欄位中插入文字(否則每次鍵盤輸入都將觸發從根節點開始的佈局)。

因為這個優化方案,所以你每改一次樣式,它就不會reflow或repaint一次。但是有些情況,如果我們的程式需要某些特殊的值,那麼瀏覽器需要返回最新的值,而會有一些樣式的改變,從而造成頻繁的reflow/repaint。比如獲取下面這些值,瀏覽器會馬上進行reflow:

offsetTop, offsetLeft, offsetWidth, offsetHeight scrollTop/Left/Width/Height clientTop/Left/Width/Height getComputedStyle(), currentStyle

我們可以做的效能優化

大家倒背如流的老話,再囉嗦一遍:儘量減少重繪重排。具體:

  1. 不要一條一條地修改DOM的樣式(用class批量操作)
  2. 快取dom節點,供後面使用(for迴圈,取html集合長度,你懂的)
  3. 把DOM離線後修改(documentFragment、虛擬dom、把它display:none再改再顯示)
  4. 儘量修改層級比較低的DOM
  5. 有動畫的DOM使用fixed或absoult的position,脫離文件流

4. 重繪與重排(重要)

4.1 重排(reflow)

重排(也叫回流)會計算頁面佈局(Layout)。某個節點Reflow時會重新計算節點的尺寸和位置,而且還有可能觸其後代節點reflow。重排後,瀏覽器會重新繪製受影響的部分到螢幕,該過程稱為重繪。另外,DOM變化不一定都會影響幾何屬性,比如改變一個元素的背景色不影響寬高,這種情況下只會發生重繪,代價較小。

當DOM的變化影響了元素的幾何屬性(寬或高),瀏覽器需要重新計算元素的幾何屬性,由於流式佈局其他元素的幾何屬性和位置也受到影響。瀏覽器會使渲染樹中受到影響的部分失效,並重新構造渲染樹。 reflow 會從根節點開始遞迴往下,依次計算所有的結點幾何尺寸和位置,在reflow過程中,可能會增加一些frame,如文字字串。DOM 樹裡的每個結點都會有reflow方法,一個結點的reflow很有可能導致子結點,甚至父點以及同級結點的reflow。

當渲染樹的一部分(或全部)因為元素的尺寸、佈局、隱藏等改變而需要重新構建。所以,每個頁面至少需要一次reflow,就是頁面第一次載入的時候。

4.2 重繪(repaint)

repaint(重繪)遍歷所有節點,檢測節點的可見性、顏色、輪廓等可見的樣式屬性,然後根據檢測的結果更新頁面的響應部分。當渲染樹中的一些元素需要更新一些不會改變元素不局的屬性,比如只是影響元素的外觀、風格、而不會影響佈局的那些屬性,這時候就只發生重繪。當然,頁面首次載入也是要重繪一次的。

光柵:光柵主要是針對圖形的一個柵格化過程。現代瀏覽器中主要的繪製工作主要用光柵化軟體來完成。所以元素重繪由這個元素和繪製層級的關係,來決定的是否會很大程度影響你的效能-,如果這個元素蓋住的多層元素都被重新繪製,效能損耗當然大。

5. paint(繪製)

在繪製階段,系統會遍歷渲染樹,並呼叫呈現器的“paint”方法,將呈現器的內容繪製成點陣圖。繪製工作是使用使用者介面基礎元件完成的 你所看見的一切都會觸發paint。包括拖動滾動條,滑鼠選擇中文字等這些完全不改變樣式,只改變顯示結果的動作都會觸發paint。paint的工作就是把文件中使用者可見的那一部分展現給使用者。paint是把layout和樣式計算的結果直接在瀏覽器視窗上繪製出來,它並不實現具體的元素計算,只是layout後面的那一步。

繪製順序:背景顏色->背景圖片->邊框->子代->輪廓

其實就是元素進入堆疊樣式上下文的順序。這些堆疊會從後往前繪製,因此這樣的順序會影響繪製。

再說回來,在樣式發生變化時,瀏覽器會盡可能做出最小的響應。因此,元素的顏色改變後,只會對該元素進行重繪。元素的位置改變後,只會對該元素及其子元素(可能還有同級元素)進行佈局和重繪。新增 DOM 節點後,會對該節點進行佈局和重繪。一些重大變化(例如增大“html”元素的字型)會導致快取無效,使得整個渲染樹都會進行重新佈局和繪製。

6. composite(重要)

概念不復雜,即是渲染層合併,我們將渲染樹繪製後,形成一個個圖層,最後把它們組合起來顯示到螢幕。渲染層合併。前面也說過,對於頁面中DOM元素的繪製是在多個層上進行的。在每個層上完成繪製過程之後,瀏覽器會將繪製的點陣圖傳送給GPU繪製到螢幕上,將所有層按照合理的順序合併成一個圖層,然後在螢幕上呈現。

對於有位置重疊的元素的頁面,這個過程尤其重要,因為一量圖層的合併順序出錯,將會導致元素顯示異常。另外,這部分主要的是這涉及到我們常說的GPU加速的問題。

說到效能優化,針對頁面渲染過程的話,我們希望的是代價最小,避免多餘的效能損失,少一點讓瀏覽器做的步驟。比如我們可以分析一下開頭的那幅圖:

image

明顯,我們改的越深,代價越大,所以我們只改最後一個流程——合成的時候,效能是最好的。瀏覽器會為使用了transform或者animation的元素單獨建立一個層。當有單獨的層之後,此元素的Repaint操作將只需要更新自己,不用影響到別,區域性更新。所以開啟了硬體加速的動畫會變得流暢很多。

因為每個頁面元素都有一個獨立的渲染程式,包含了主執行緒和合成執行緒,主執行緒負責js的執行、CSS樣式計算、計算Layout、將頁面元素繪製成點陣圖(Paint)、傳送點陣圖給合成執行緒。合成執行緒則主要負責將點陣圖傳送給GPU、計算頁面的可見部分和即將可見部分(滾動)、通知GPU繪製點陣圖到螢幕上。加上一個點,GPU對於動畫圖形的渲染處理比CPU要快,那麼就可以達到加速的效果。

注意不能濫用GPU加速,一定要分析其實際效能表現。因為GPU加速建立渲染層是有代價的,每建立一個新的渲染層,就意味著新的記憶體分配和更復雜的層的管理。並且在移動端 GPU 和 CPU 的頻寬有限制,建立的渲染層過多時,合成也會消耗跟多的時間,隨之而來的就是耗電更多,記憶體佔用更多。過多的渲染層來帶的開銷而對頁面渲染效能產生的影響,甚至遠遠超過了它在效能改善上帶來的好處。

7. 瀏覽器載入的時間線(重要)

這是補充前面的html解析為dom部分的內容。

  1. 建立document物件,解析html,將元素物件和文字內容新增到文件中,此時document.readyState = 'loading'
  2. 遇到link外部css的時候,建立新的執行緒非同步載入,繼續解析html
  3. 遇到有src的scripts(沒有async和defer標記)載入外部的js時,同步載入並阻塞解析html,而且載入完馬上執行
  4. 遇到設定async和defer的script,建立新的執行緒非同步載入,繼續解析html。async載入完馬上執行,defer在DOMContentLoaded前執行
  5. 遇到帶有src的img,解析dom結構,再非同步載入src的圖片資源,不會等待img載入完成繼續解析文件。另外,img要等待css載入完才解碼,所以css阻塞圖片的呈現,類似於js阻塞html解析一樣。可以想一下,如果css被設定為display:none,還有意義嗎?所以此時雖然對後臺有請求但不解碼
  6. 文件解析完畢,document.readyState = 'interactive'
  7. 此時帶有defer的js開始按順序執行
  8. DOMContentLoaded觸發,程式從同步指令碼執行轉化為事件驅動階段(類似ele.onclick = handel已經開始生效)
  9. 當所有的script載入完成並且成功執行、img和css載入完畢,document.readyState = 'completed',觸發onload事件
  10. 非同步響應ui行為,開始互動

補充:script和link標籤的問題

明顯,CSSOM樹和DOM樹是互不關聯的兩個過程。平時我們把link標籤放部頭而script放body尾部,因為js阻塞阻塞DOM樹的構建。但是js需要查詢CSS資訊,所以js還要等待CSSOM樹構建完才可以執行。這就造成CSS阻塞了js,js阻塞了DOM樹構建。所以我們只要設定link的preload來預載入css檔案,解決了js執行時CSSOM樹還沒構建好的阻塞問題。當然,script非同步載入也是另外的方法。

總的來說,參考一下很多人說過的規律:

  • CSS 不會阻塞 DOM 的解析,但會阻塞 DOM 渲染。
  • JS 阻塞 DOM 解析,但瀏覽器會"偷看"DOM,提前下載資源。
  • 瀏覽器遇到 script且沒有defer或async屬性的標籤時,會觸發頁面渲染,因而如果前面CSS資源尚未載入完畢時,瀏覽器會等待它載入完畢在執行指令碼。

相關文章