瀏覽器是怎樣渲染網頁的呢?

seconp發表於2020-11-21

目錄

Document Object Model (DOM)

CSS Object Model (CSSOM)

Render Tree

Rendering Sequence

Layout operation

Paint operation

Compositing operation

介面渲染順序圖

Browser engines

Rendering Process in browsers

Parsing and External Resources

Parser-Blocking Scripts

Render-Blocking CSS

Document’s DOMContentLoaded Event

Window’s load event


有時候我們在使用某個網站的時候會出現影響使用者體驗的共性問題,例如:網站載入速度過慢、一直在等待檔案的載入、載入出來了介面卻沒有樣式等。為了避免開發人員開發這種網站,我們需要深入理解瀏覽器渲染介面的生命週期。

Document Object Model (DOM)

首先我們需要理解什麼是DOM,瀏覽器向伺服器傳送請求獲取HTML資料,伺服器以二進位制位元組流的形式向瀏覽器返回HTML文字,這個response的header中有這樣的attribute:Content-Type:text/html;charset=UTF-8。text/html是一種MIME Type,它告知瀏覽器這種MIME Type是HTML Document。charset=UTF-8告知瀏覽器該MIME Type檔案需要使用UTF-8的編碼方式解碼。根據這些資訊,瀏覽器就能將二進位制位元組流轉化為我們看到的HTML Document。

如果丟失response header中的text/html,瀏覽器就不能理解怎樣處理該MIME Type,這個時候二進位制位元組流將會被渲染普通文字格式。但是如果一切正常,經過瀏覽器對該MIMET Type檔案的轉化,典型的HTML Document最終看起來會是這樣:

<!DOCTYPE HTML>
<html>
    <head>
        <title>Rendering Test</title>
      
        <!-- stylesheet -->
        <link rel="stylesheet" href="./style.css"/>
    </head>
    <body>
        <div class="container">
          <h1>Hello World!</h1>
          <p>This is a sample paragraph.</p>
        </div>
      
        <!-- script -->
        <script src="./main.js"></script>
    </body>
</html>

在上面的程式碼段中,該網頁依賴於為網頁提供樣式的style.css和為網頁提供操作的main.js。在style.css新增一些炫酷的樣式,我們的網頁看起來如下圖所示:

但是問題仍然存在啊,瀏覽器是怎樣將平平無奇的只包含文字的HTML檔案渲染成如此炫酷的介面呢?為了解決這個問題我們需要從DOM、CSSOM、Render Tree入手。

無論何時瀏覽器解析HTML程式碼,遇到HTML、body、div等元素,他都會建立與之對應的JavaScript Node物件,最終所有的元素都會被轉化為JavaScript物件。由於每個HTML元素都有不同的屬性,因此將通過不同的類(建構函式)建立Node物件。例如:div對應的Node物件HTMLDivElement繼承自Node類,我們可以使用chrome的DevTools來做如下測試:

瀏覽器為每個元素建立完物件之後,它將會為這些Node物件建立一個樹形結構。由於在HTML文件中元素之間互相巢狀,所以瀏覽器需要複製文件中的元素但是使用之前建立的Node物件來建立樹形結構。這個步驟將有助於瀏覽器在整個生命週期內有效地呈現和管理網頁。

這就是DOM Tree。其結構如上圖所示。

DOM Tree最頂端的元素是html,其分支是元素在文件中出現和巢狀來顯示的。不論什麼時候解析到HTML元素,瀏覽器都會建立與之對應的Node物件。

DOM節點並不一定總是HTML元素。 當瀏覽器建立DOM樹時,它還將諸如註釋,屬性,文字之類的內容另存為樹中的單獨節點。 但為簡單起見,我們僅考慮HTML元素(又稱為DOM元素)的DOM節點。想要了解DOM節點型別可以參考這裡

DOM Tree通過例項物件的七個屬性描述節點之間的關係,構成層次結構

         1)  ownerDocument屬性:  該屬性指向整個節點樹中的文件節點

         2)  parentNode屬性:  該屬性指向節點樹中該節點的父節點

         3)  previousSibling屬性:  該屬性指向節點樹中該節點的左兄弟節點

         4)  nextSibling屬性:  該屬性指向節點樹中該節點的右兄弟節點

         5)  childNodes屬性:  該屬性指向節點樹中該節點的子節點NodeList集合

         6)  firstChild屬性:  該屬性指向節點樹中該節點的子節點NodeList集合中的第一個位元組點

         7)  lastChild屬性:  該屬性指向節點樹中該節點的子節點NodeList集合中的最後一個位元組點

可以通過Chrome DevTools來觀察節點之間的繼承關係:

JavaScript不知道什麼是DOM,DOM不是JavaScript規範的一部分。DOM是瀏覽器提供的高階Web API,用於高效地呈現網頁並將其公開顯示給開發人員,以便開發者動態操縱DOM元素。

使用DOM API開發人員可以對HTML元素進行增刪改查的操作,可以在記憶體中建立或者拷貝HTML元素,在不渲染DOM Tree的情況下操作HTML元素。這使開發人員能夠構建具有豐富使用者體驗的高度動態的網頁。

CSS Object Model (CSSOM)

當我們設計網站的時候,我們需要將其設計地盡善盡美。為了達到這個目標我們在HTML元素上提供一些樣式。在本頁面中我們使用的是Cascading Style Sheets(級聯樣式)。使用CSS選擇器我們能夠定向操縱目標元素的樣式。

外部CSS檔案、通過<style>內嵌CSS樣式、在HTML元素上使用style屬性、使用JavaScript 應用在HTML元素上的方法是不同的,但是最終瀏覽器繁瑣地將CSS樣式應用在DOM元素上。

在構建好DOM Tree之後,瀏覽器會載入所有的CSS樣式(外部CSS檔案,<style>內嵌樣式,行內style屬性,使用者代理樣式等)來構建一個CSSOM。CSSOM是一個樹形結構的CSS物件模型。

CSSOM Tree上的每一個節點都會儲存CSS樣式資訊,最終會被應用DOM Tree的目標元素上。CSSOM不包含無法在螢幕上顯示的<meta>、<script>等DOM元素。

瀏覽器本身會具有它自己的樣式檔案,定義我們沒有自定義CSS樣式的時候需要顯示的樣式。這被稱為使用者代理樣式。瀏覽器在計算樣式的時候會讓使用者自定義的樣式覆蓋使用者代理樣式。

根據W3C CSS的標準,即使使用者和瀏覽器沒有定義該CSS屬性(例如:display),該屬性也會有預設值。在選擇CSS屬性的預設值時,如果某個屬性符合W3C文件中提到的繼承條件,則將使用一些繼承規則。

例如,如果HTML元素缺少color和font-size這些屬性,則DOM元素會繼承父級的值。 因此,您可以想象在HTML元素及其所有子元素都擁有這些屬性。 這就是所謂的級聯樣式,這也是CSS是級聯樣式表的縮寫的原因。 這也是為什麼瀏覽器構造CSSOM(一種類似樹的結構以根據CSS級聯規則計算樣式)的原因。

通過Chrome DevTools,從左側皮膚中選擇任何HTML元素,然後在右側皮膚中單擊“計算”選項卡可以觀察該元素的樣式。

為上面的HTML檔案新增如下樣式:

html {
    padding: 0;
    margin: 0;
}

body {
    font-size: 14px;
}

.container {
    width: 300px;
    height: 200px;
    color: black;
}

.container > h1 {
    color: gray;
}

.container > p {
    font-size: 12px;
    display: none;
}

最終會構建如下CSSOM Tree:

Render Tree

Render Tree也是通過將DOM和CSSOM樹組合在一起而構建的樹狀結構。 瀏覽器必須計算每個可見元素的佈局並將其繪製在螢幕上,因為該瀏覽器需要使用此Render Tree。 未構建Render Tree的情況下螢幕上不會顯示任何內容,這就是我們同時需要DOM和CSSOM樹的原因。

由於“渲染樹”是在螢幕上顯示的內容的底層表示,因此它不會包含不佔據畫素矩陣中任何區域的節點。 例如,display:none; 該元素的尺寸為0px X 0px,因此該元素不會出現在“渲染樹”中。

從上圖可以看到,Render-Tree結合了DOM和CSSOM進而生成樹狀結構,其中僅包含將在螢幕上列印的元素。

在CSSOM中,位於div內的p元素屬性為display:none,因此它及其子級不會出現在“渲染樹”中,因為它在螢幕上不佔空間。 但是,如果元素的屬性為visibility:hidden或opacity:0,它們將佔據螢幕上的空間,因此它們會出現在“渲染樹”中。

與DOM API允許訪問由瀏覽器構造的DOM Tree中的DOM元素不同,CSSOM對使用者隱藏。 但是,由於瀏覽器將DOM和CSSOM結合在一起形成了“Render Tree”,因此瀏覽器通過在DOM元素本身上提供高階API來公開DOM元素的CSSOM節點。 這使開發人員可以訪問或更改CSSOM節點的CSS屬性。

想要檢視怎樣通過JavaScript對CSS進行操作,可以參考:CSS Tricks Article&CSS Typed Object

Rendering Sequence

現在,我們對DOM,CSSOM和Render Tree有了很好的瞭解,讓我們一起來了解瀏覽器如何使用它們來呈現網頁。 對這個過程的簡單瞭解對於任何Web開發人員都是至關重要的,因為它將幫助讓我們設計的網站獲得良好的使用者體驗和效能。

載入網頁後,瀏覽器將首先讀取HTML文字並從中構造DOM樹。 然後,它處理CSS(無論是嵌入式CSS,嵌入式CSS還是外部CSS),並從中構造CSSOM樹。
構造完這些樹後,便會從中構造出渲染樹。 一旦構建了Render Tree,瀏覽器便開始在螢幕上列印單個元素。

Layout operation

首先,瀏覽器建立每個單獨的“渲染樹”節點的佈局。 佈局包括每個節點的大小(以畫素為單位)以及它將在螢幕上列印的位置。 由於瀏覽器正在計算每個節點的佈局資訊,因此此過程稱為佈局(layout)。

此過程也稱為重排(reflow)或瀏覽器重排(browser reflow),並且在滾動,調整視窗大小或操作DOM元素時也可能發生。 這是可以觸發元素的佈局/重排的事件列表

我們應該避免網頁經歷多次佈局操作,因為這是一項繁雜的操作。 這是保羅·劉易斯(Paul Lewis)發表的一篇文章,他談到了如何避免複雜而昂貴的佈局操作也就是佈局顛簸(layout thrashing)。

Paint operation

到目前為止,我們具有了需要在螢幕上列印的幾何分佈矩陣。 由於“渲染樹”中的元素(或子樹)可以彼此重疊,並且它們可以具有CSS屬性,這些屬性使它們經常更改外觀,位置或形狀(例如動畫),因此瀏覽器會為其建立一個圖層(layer)。

建立圖層可幫助瀏覽器在網頁的整個生命週期中有效執行繪畫操作,例如在滾動或調整瀏覽器視窗大小時。圖層還可以幫助瀏覽器正確地按照開發人員的意願按順序(沿z軸)繪製元素。

現在我們有了圖層,我們可以將它們組合起來並在螢幕上繪製它們。但是瀏覽器並不會一次繪製所有圖層。會分別繪製每個圖層。
在每一層內部,瀏覽器會填充元素的任何可見屬性(例如邊框,背景色,陰影,文字等)的各個畫素。此過程也稱為光柵化(raster)。為了提高效能,瀏覽器可以使用不同的執行緒來執行光柵化。

Photoshop中的圖層可以用來類比瀏覽器中的呈現網頁的圖層。您可以通過Chrome DevTools視覺化網頁上的不同圖層。開啟DevTools,然後從more tools選項中選擇“Layers”。您也可以從該皮膚中視覺化圖層邊框。

柵格化(raster)通常在CPU中完成,這樣的速度緩慢且CPU資源本就稀缺。現在我們有了新的技術可以在GPU中進行柵格化進而增強效能。這篇文章詳細介紹了該主題。關於layer同時可以閱讀這一篇文章來加強理解

Compositing operation

到目前為止,我們還沒有在螢幕上繪製單個畫素。 我們所擁有的是不同的層(bitmap images),應該以特定的順序在螢幕上繪製它們。 在合成操作中,這些層被髮送到GPU,最後將其繪製在螢幕上。

傳送整個圖層以進行繪製效率很低,所以每次進行reflow(layout)或repaint時都必須進行此操作。 因此,將一層分解為不同的塊(tiles),然後將其繪製在螢幕上。 您還可以在Chrome DevTool 的 Rendering皮膚中視覺化這些塊(tiles)。

介面渲染順序圖

這一系列事件也被稱為critical rendering path

Browser engines

建立DOM Tree,CSSOM Tree和處理渲染邏輯的工作是使用瀏覽器引擎(也稱為渲染引擎或佈局引擎)完成的。 該瀏覽器引擎包含所有將網頁從HTML程式碼渲染為螢幕上的實際畫素所需要的元素和邏輯。

如果你聽到有人在談論WebKit,那麼他們在談論瀏覽器引擎。 WebKit被Apple的Safari瀏覽器使用,並且是Google Chrome瀏覽器的預設渲染引擎。

Rendering Process in browsers

HTML,CSS或JavaScript,這些語言是由某個實體或某個組織標準化的。 但是,瀏覽器如何統籌管理它們並且在螢幕上呈現出來沒有相關的標準。 Google Chrome瀏覽器的引擎功能可能與Safari瀏覽器的引擎功能不同。

因此,很難預測它們在特定瀏覽器中的渲染順序及其背後的機制。 但是,HTML5規範已經做出一些努力來標準化渲染過程在理論上應該如何工作,但是瀏覽器如何遵循此標準完全取決於各家廠商。

儘管存在這些不一致,但仍有一些通用原則被所有瀏覽器遵循。讓我們一起來了解瀏覽器在螢幕上呈現內容的常用方法以及此過程的生命週期事件。為了理解此過程,我準備了一個小專案來測試不同的渲染方案(下面的連結)。

course-one/browser-rendering-test

Parsing and External Resources

解析的過程就是瀏覽器引擎不斷讀取HTML Document並構建DOM Tree的過程。因此這個過程也被稱為DOM parsing,處理者也被稱為解析器(DOM parser)。

許多瀏覽器提供DOM parser API構建DOM Tree,DOMParser類的一個例項表示一個DOM解析器,使用parseFromString原型方法,我們可以將原始HTML文字解析為一個DOM Tree。

瀏覽器對網頁發出請求,伺服器做出響應,其中一些檔案的的頭部會被設定為Content-Type:text/ HTML,只要在該文字中載入出來字元(某一時刻該文字可能只載入出來了幾個字元或者幾行字元等等),瀏覽器就可以開始解析HTML。因此,瀏覽器可以增量地構建DOM樹,一次一個節點。從上到下解析HTML,而不是在中間的任何位置,因為HTML表示一個巢狀的樹狀結構。

 

在上面的例子中,我們通過Chrome DevTools限制網速並訪問incremental.html,瀏覽器會花費大量時間去載入檔案,然後它將會從文字的第一個位元組開始構建DOM Tree並print到介面上。

你可以觀察Chrome Devtools中的效能(performance) 皮膚,你可以看到在Timing皮膚中發生的一些事件。我們通常稱這些事件為“效能衡量標準”(performance metrics)。當這些事件儘可能地靠近彼此並且發生得越早,使用者體驗就越好。

FPFirst Paint的首字母縮寫,意思是瀏覽器開始在螢幕上列印東西的時間(可以簡單抽象想象為列印body的背景色的第一個畫素)。

FCPFirst Contentful Paint的首字母縮寫,意思是瀏覽器呈現文字或影像等內容的第一個畫素的時間。LCP是“Largest Contentful Paint的縮寫,意思是瀏覽器渲染大文字或影像的時間。

你可能聽說過FMP(first meaningful paint),它也是一個類似於LCP的度量標準,但它已經從Chrome中刪除,取而代之的是LCP

L表示由瀏覽器在視窗物件上發出的onload事件。類似地,DCL表示在文件物件上發出的DOMContentLoaded事件。

當瀏覽器遇到外部資源,如一個JavaScript指令碼檔案< script src = " url " > < /script>,一個CSS樣式表檔案< link rel = "stylesheet" href = " url " / >,一個img檔案< img src = " url " / >或任何其他外部資源,瀏覽器會在後臺下載檔案。

其中讀者需要了解的也是最重要的是DOM解析通常發生在main thread上。 因此,如果主JavaScript執行執行緒繁忙,DOM會直到main thread空閒才開始解析。 您可能會問為什麼這是如此重要? 因為指令碼元素是parse-blocking的。 除指令碼(.js)檔案請求外,其他外部檔案請求(例如影像,樣式表,pdf,視訊等)都不會阻止DOM構建/解析。

Parser-Blocking Scripts

parser-blocking script是會使HTML停止解析的指令碼檔案/程式碼。 當瀏覽器遇到指令碼元素(如果它是嵌入式指令碼)時,它將首先執行該指令碼,然後繼續解析HTML以構造DOM Tree。 因此,所有嵌入式指令碼都是parser-blocking的。

如果指令碼元素是外部指令碼檔案,瀏覽器將開始從主執行緒下載外部指令碼檔案,但是它將停止執行主執行緒,直到完成下載。 這意味著在下載指令碼檔案之前不能進行DOM解析。

一旦下載了指令碼檔案,瀏覽器將首先在主執行緒上執行下載的指令碼檔案,然後繼續進行DOM解析。如果瀏覽器再次在HTML中找到另一個指令碼元素,它將執行相同的操作。為什麼瀏覽器必須停止DOM解析來下載並執行JavaScript?

因為JavaScript可以在執行時(runtime)訪問DOM API,我們可以使用JavaScript訪問和操作DOM元素。這就是動態Web框架(dynamic web frameworks,例如React和Angular)的工作方式。但是,如果瀏覽器並行執行DOM解析和指令碼執行,則DOM解析器執行緒和主執行緒之間可能存在競爭情況(race conditions,對共享資源的同時訪問會出現競爭情況),這就是為什麼DOM解析必須在主執行緒上進行的原因。

但是,在大多數情況下,完全不必在後臺下載指令碼檔案時停止DOM解析。因此,HTML5為我們提供了script標籤的async屬性。當DOM解析器遇到具有async屬性的外部指令碼元素時,即使在後臺下載指令碼檔案,也不會停止DOM解析過程。但是,一旦下載了檔案,解析將停止並執行指令碼。

我們還為script元素提供了defer屬性,該屬性的作用類似於async屬性,但與async屬性不同的是,即使檔案已完全下載,該指令碼也不會立刻執行。解析器解析完所有HTML之後,將執行所有具有defer屬性的指令碼,這意味著DOM樹已完全構建。與非同步指令碼不同,所有延遲指令碼都按照它們在HTML文件(或DOM樹)中出現的順序執行。

所有常規指令碼(嵌入式或外部)在停止DOM的構建時都被解析器阻止。在下載完成之前,它們不會阻止解析器執行。下載完成後,它將立即阻止解析器執行。但是,所有延遲指令碼(deferred scripts)都是不會阻止解析器的執行,它們在完全構建DOM樹之後執行。

在上面的示例中,parser-blocking.html檔案在30個元素之後開始載入指令碼,這就是為什麼瀏覽器首先會顯示30個元素,停止DOM解析並開始載入指令碼檔案。 第二個指令碼檔案沒有延遲,因為它具有defer屬性,它將在DOM樹完全構建後執行。

如果我們檢視“效能”皮膚,則FP和FCP會盡可能提前顯示,因為只要有一些HTML內容可用,瀏覽器就會開始構建DOM樹,在螢幕上儘可能呈現一些畫素。

LCP在5秒鐘後發生,因為parser-blocking script將DOM parsing阻止了5秒鐘(其下載時間佔用5秒鐘),並且當DOM解析器被阻止時,螢幕上僅呈現了30個文字元素,這不足命名為 largest paint(根據Google Chrome標準)。 一旦下載並執行了指令碼,便會重新進行DOM解析,並在螢幕上呈現大量內容,從而引發LCP事件。

parser blocking 經常和 render blocking 關聯起來,但是這兩者是不同的,因為DOM Tree沒有構建成功之前rendering是不會發生的。

某些瀏覽器可能包含speculative parsing,其中HTML parsing(而不是DOM Tree construction)被裝載到單獨的執行緒中,以便瀏覽器可以儘量讀取諸如link,script,img等元素,並下載這些資源。

如果你有三個指令碼檔案,最好將這三個檔案的載入放在一起。解析第一個指令碼的時候,DOM解析器無法讀取第二個指令碼元素,因此在下載第一個指令碼之前,瀏覽器將無法開始下載第二個指令碼。 我們可以使用async標籤解決此問題,但這樣一來就不能保證非同步指令碼按順序執行。

之所以稱為推測性解析(speculative parsing),是因為瀏覽器會猜測將來可能會載入某些特定的資源,因此會在後臺將其載入。 但是,如果某些JavaScript處理DOM並使用外部資源操作該元素,則該策略將失敗,並且載入過的這些檔案將一無是處。

每個瀏覽器都有自己的策略,因此不能保證何時或是否會進行推測解析。 但是,我們可以要求瀏覽器使用<link rel =“ preload”>標籤提前載入一些資源。

Render-Blocking CSS

我們在實際應用過程中竟然發現CSS也可以阻止DOM解析????真的是這樣嗎????好吧,為了弄懂其中原理,我們需要了解渲染過程。

瀏覽器內部的瀏覽器引擎根據從伺服器以文字文件形式接收的HTML文字構造DOM樹。同樣,它從樣式檔案(例如,外部CSS檔案或HTML中的嵌入式CSS)構造CSSOM Tree。

DOM Tree和CSSOM Tree的構造都發生在主執行緒上,並且這些樹可以同時構造。最終他們會共同形成Render Tree,用於在螢幕上paint內容。Render Tree隨著DOM Tree的逐漸構建而逐漸構建。

正如我們所瞭解的那樣,DOM Tree的生成是incremental,這意味著在瀏覽器讀取HTML時,它將向DOM Tree中新增DOM元素。但是CSSOM Tree不是這種情況。與DOM Tree不同,CSSOM Tree的構建不是增量的,必須以特定的方式進行。

當瀏覽器找到<style>塊時,它將解析所有嵌入式CSS並使用新的CSS規則更新CSSOM Tree。之後,它將繼續以正常方式解析HTML。內聯樣式也是如此。但是,當瀏覽器遇到外部樣式表檔案時,情況會截然不同。與外部指令碼檔案不同,外部樣式表檔案不會阻止解析器執行,因此瀏覽器可以在後臺默默下載,並且DOM解析將繼續進行。

但是與HTML檔案(用於DOM構建)不同,瀏覽器不會一次只處理一個位元組的樣式表檔案內容。這是因為瀏覽器在讀取CSS檔案時無法逐步構建CSSOM Tree。原因是檔案末尾的CSS規則可能會覆蓋檔案頂部寫的CSS規則。

因此,如果瀏覽器在解析樣式表內容時開始逐步構建CSSOM Tree,則由於相同的CSSOM節點將被更新,這會導致一顆CSSOM Tree的多次渲染,因為後面的樣式將覆蓋前面的樣式。如果事實是這樣的話,我們在瀏覽器上載入網頁的時候將會看到元素的佈局,顏色等樣式不斷變化,直到某一時刻才穩定下來。由於CSS樣式是級聯的,因此一項規則更改也會影響許多元素。

因此,瀏覽器不會增量(incrementally)處理外部CSS檔案,並且在處理樣式表中的所有CSS規則之後,會立即進行CSSOM Tree更新。 CSSOM樹更新完成後,將更新“Render Tree”,然後將其呈現在螢幕上。

CSS也是一種阻止渲染的資源。瀏覽器發出獲取外部樣式表的請求後,將停止“Render Tree”構建。因此,關鍵渲染路徑(Critical Rendering Path ---- CRP)會被卡住,在螢幕上不會渲染任何內容,如下所示。但是,在後臺下載樣式表時,仍在進行DOM樹構建。

瀏覽器本來可以使用CSSOM Tree的“舊狀態”來生成“Render Tree”,因為解析HTML是逐步的,在螢幕上渲染也是逐步的。但這有很大的缺點,在這種情況下,一旦下載並解析了樣式表,並且更新了CSSOM,Render Tree將被更新並呈現在螢幕上。現在,使用“舊狀態”樣式的CSSOM重新使用“新狀態”繪製可能導致出現“無狀態”樣式內容(Flash of Unstyled Content----FOUC),這對於UX(使用者體驗)來說是非常不好的。

因此,瀏覽器將等待,直到樣式表被載入並解析。解析了樣式表並更新了CSSOM之後,將更新“Rneder Tree”,並且將解除關鍵渲染路徑(CRP)的阻塞,從而在螢幕上繪製“Rneder Tree”。由於這個原因,建議HTML文件儘量在header載入所有外部樣式表。

讓我們想象一個場景,其中瀏覽器已經開始解析HTML,並且遇到一個外部樣式表檔案。它將開始在後臺下載檔案,阻止關鍵渲染路徑(CRP)並繼續進行DOM解析。但是隨後它遇到了一個指令碼標籤,因此它將開始下載外部指令碼檔案並阻止DOM解析。現在,瀏覽器處於空閒狀態,等待樣式表和指令碼檔案完全下載。

但是如果外部指令碼檔案已完全下載,而樣式表仍在後臺下載。瀏覽器應該執行指令碼檔案嗎?這樣做有什麼危害嗎?

CSSOM提供了一個高階JavaScript API與DOM元素的樣式進行互動。例如,您可以使用elem.style.backgroundColor屬性讀取或更新DOM元素的背景顏色。與elem元素關聯的樣式物件已經公開了CSSOM API,並且還有許多其他API也可以做到這一點(詳情請閱讀此css-tricks文章)。

由於在後臺下載樣式表,因此JavaScript仍可以執行,因為主執行緒沒有被裝入的樣式表阻塞。如果我們的JavaScript程式(通過CSSOM API)訪問DOM元素的CSS屬性,我們將獲得適當的值(根據CSSOM的當前狀態)。

但是一旦下載並解析了樣式表,CSSOM Tree將會更新,我們的JavaScript現在具有該元素的“舊狀態”樣式,因為新的CSSOM更新可能會更改該DOM元素的CSS屬性。因此,在下載樣式表時執行JavaScript是不安全的。

根據HTML5規範,瀏覽器可以下載指令碼檔案,但是除非解析了所有以前的樣式表,否則它不會執行。當樣式表阻止指令碼的執行時,它稱為指令碼阻止樣式表(script-blocking stylesheet)或指令碼阻止CSS(script-blocking CSS)。

在上面的示例中,script-blocking.html包含一個連結標記(用於外部樣式表),後跟一個指令碼標記(用於外部JavaScript)。 在這裡,指令碼的下載速度非常快,沒有任何延遲,但是樣式表的下載需要6秒鐘。 因此,即使指令碼已完全下載(如“Network”皮膚中所見),瀏覽器也不會立即執行該指令碼。 僅在載入樣式表之後,我們才能看到指令碼記錄的Hello World訊息。

像async或defer屬性使指令碼元素成為non-parser-blocking document,也可以使用media屬性將外部樣式表標記為non-render-blocking。 使用media屬性,瀏覽器可以智慧決定何時載入樣式表。

Document’s DOMContentLoaded Event

DOMContentLoaded(DCL)事件標記了瀏覽器何時從現有可用的HTML元素成功構建了完整的DOM Tree。 但是,觸發DCL事件涉及許多可變因素。

document.addEventListener( 'DOMContentLoaded', function(e) {
    console.log( 'DOM is fully parsed!' );
} );

如果我們的HTML不包含任何指令碼,則不會阻止DOM解析,並且DCL將會在瀏覽器能夠解析整個HTML文件時迅速啟動。如果我們有parser-blocking指令碼,則DCL必須等待,直到所有parser-blocking指令碼都下載並執行。

將樣式表應用在圖片上時,情況變得有些複雜。即使您沒有外部指令碼,DCL也會等到所有樣式表都載入完畢。由於DCL標誌著整個DOM樹準備就緒的時間點,但是除非CSSOM也已完全構建,否則DOM Tree將無法安全訪問(進而獲取樣式資訊)。因此,大多數瀏覽器都等到所有外部樣式表都載入並解析完畢再觸發DCL。

Script-blocking stylesheet顯然會延遲DCL。在這種情況下,由於指令碼正在等待樣式表載入,因此不會構建DOM樹。

DCL是網站效能指標之一。我們應該將DCL發生時間優化地儘可能的小。最佳實踐之一是在可能的情況下對指令碼元素使用defer和async標籤,以便在後臺下載指令碼時瀏覽器可以執行其他操作。其次,我們應該優化the script-blocking and render-blocking stylesheets。

Window’s load event

JavaScript可以阻止DOM樹的生成,但是外部樣式表和檔案(例如影像,視訊等)卻並非如此。

DOMContentLoaded事件標記了完全構建DOM樹並且可以安全訪問的時間點,window.onload事件標記了外部樣式表和檔案、Web應用程式已完成下載的時間點。

window.addEventListener( 'load', function(e) {
  console.log( 'Page is fully loaded!' );
} )

在上面的示例中,rendering.html檔案的頭部具有一個外部樣式表,下載該樣式表大約需要5秒鐘。該樣式表將阻止接下來任何會被呈現的內容(因為它阻止了CRP),因此FP和FCP在5秒鐘後發生

此後,我們有一個img元素,完全載入大約需要10秒鐘。因此,瀏覽器將繼續在後臺下載此檔案,並繼續進行DOM解析和渲染(因為外部影像資源既不會阻止解析器也不會阻止渲染)。

接下來,我們有三個外部JavaScript檔案,分別需要3秒鐘,6秒鐘和9秒鐘進行下載。它們不是非同步的,這意味著總載入時間應接近18秒,因為在執行前一個指令碼之前,後續指令碼不會開始下載。但是,檢視DCL事件,我們的瀏覽器似乎已經使用推測性策略下載了指令碼檔案,因此總載入時間接近9秒。

最後一個可能影響DCL的檔案是最後一個指令碼檔案,其載入時間為9秒(因為樣式表已在5秒內下載完畢),因此DCL事件發生在9.1秒左右。

我們還擁有另一個外部資源,即影像檔案,它一直在後臺載入。完全下載(需要10秒)後,所以在10.2秒後會觸發視窗的載入事件,這表明網頁(應用程式)已完全載入。

本文的內容主要來自於:How the browser renders a web page?

相關文章