1. 程式人生 > 實用技巧 >探索瀏覽器對於HTML的渲染原理(過程)

探索瀏覽器對於HTML的渲染原理(過程)

原文連結:https://github.com/FIGHTING-TOP/FE-knowlodge-base/issues/6

探索目的

為了更好地優化我們前端頁面的效能,特對基礎原理進行考究

大致過程

從瀏覽器獲取到HTML檔案開始,瀏覽器會經歷解析、渲染、互動三大階段;

解析

瀏覽器會在收到HTML檔案的第一次響應包後開始解析(即使該HTML大於14kb),解析過程包括DOM樹和CSSOM樹的構建、資源的預載入(通過預載入掃描器非同步載入)、JavaScript 編譯以及構建輔助功能樹。DOM包含了頁面的所有內容,CSSOM包含了頁面的所有樣式。

渲染

渲染過程包括Style、Layout、Paint以及還可能會有Compositing這些階段,
渲染器會在DOM樹和CSSOM樹構建好之後,將兩棵樹組合成一個render樹,這個過程會計算所有可以顯示標籤的樣式,可以顯示的標籤包括從body

開始(包括body)沒有display: none的所有節點,包含帶有visibility: hidden的節點。

佈局是瀏覽器從根節點開始遍歷整棵render樹,計算每個節點的尺寸和位置;第一次確定節點的大小和位置稱為佈局。隨後對節點大小和位置的重新計算稱為迴流。如果佈局完成後有圖片載入完成並且該圖片沒有指定大小,這樣就會造成迴流。
繪製是將佈局階段生成的render樹的多有節點轉換成螢幕上的實際畫素,包括文字、顏色、邊框、陰影和替換的元素(如按鈕和影象)。

在這個過程中,瀏覽器會將佈局樹中的元素分解為多個層,將內容提升到GPU上的層(脫離CPU上的主執行緒),從而提高繪製和重新繪製的效能。每一個帶有一些特定的CSS屬性的元素和一些特定標籤元素都可以例項化一個層,像和元素,以及任何帶有opacity``,3D轉換

will-changeCSS屬性的元素都會和它們的子節點單獨繪製一個層,當然,如果子節點滿足以上條件則會再單獨例項一個層。
瀏覽器針對處理CSS動畫和不會很好地觸發重排(因此也導致重新繪製)的動畫屬性進行了優化。為了提高效能,可以將被動畫化的節點從主執行緒移到GPU上。將導致合成的屬性包括3D transforms(transform: translateZ(), rotate3d(),etc.),animating transformopacity,position: fixedwill-change,和filter。一些元素,例如<video>,<canvas>
<iframe>,也位於各自的圖層上。 將元素提升為圖層(也稱為合成)時,動畫轉換屬性將在GPU中完成,從而改善效能,尤其是在移動裝置上。

互動

當我們看到頁面顯示出來後,整個頁面的所有渲染工作可能並沒有完成,因為這時頁面可能還無法進行點選,滾動,觸控等操作,因為這個時候可能還有js沒有執行完,也就是主執行緒仍在佔用狀態,特別是像綁定了window.onLoad這種的js邏輯。
在測試頁面效能的時候,有一項重要的指標就是TTI(Time to Interactive)是從第一個請求導致DNS查詢和SSL連線到頁面可互動時所用的時間。

webkit 渲染流程圖

gecko 渲染流程圖

問題探究

CSS的載入會阻塞頁面的渲染嗎?

提出假設

我們來分析一下,從上面的頁面渲染流程來看,HTML的渲染過程是將解析階段生成的DOM樹和CSSOM樹組合成一個render樹,那麼CSS的載入肯定是會阻塞CSSOM樹的建立,那沒有CSSOM樹也就沒辦法合成render樹,因此也就沒辦法渲染,所以CSS的載入是會阻塞頁面的渲染的。

驗證假設

我們來寫段程式碼測試一下

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script>
        console.log('走js這裡了')
    </script>
    <link rel="stylesheet" href="https://www.google.com/123123.css">
</head>
<body>
    <h1>Hello World!</h1>
</body>
</html>

執行結果

從結果來看,當css檔案請求沒有結束之前頁面是空白的,等css載入失敗後頁面才顯示內容,這就說明我們的假設成立。
在這個結果中,也可以看出css沒有載入結束之前,js被執行了,那我們將js程式碼寫在link標籤的後面,他還會被執行嗎?

css載入會阻塞j後面的s執行嗎?

直接測試

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="https://www.google.com/123123.css">
    <script>
        console.log('走js這裡了')
    </script>
</head>
<body>
    <h1>Hello World!</h1>
</body>
</html>

執行結果

結果來看js是在css載入後執行了,則說明css的載入阻塞了後面js的執行。

那麼如果不是內嵌的js,使用src來引入一個js檔案呢?

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="./test000.js"></script>
    <script>
        console.log('走link標籤前面的內嵌js了')
    </script>
    <link rel="stylesheet" href="https://www.google.com/123123.css">
    <script src="./test111.js"></script>
    <script>
        console.log('走link後面的內嵌js了')
    </script>
</head>
<body>
    <h1>Hello World!</h1>
</body>
</html>

在test000.js中這樣寫

console.log('This is test000.js')

在test111.js中這樣寫

console.log('This is test111.js')

執行結果

結果是一樣的

那如果使用了defer或者async屬性呢?

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="./test000.js" defer></script>
    <script>
        console.log('走link標籤前面的內嵌js了')
    </script>
    <link rel="stylesheet" href="https://www.google.com/123123.css">
    <script src="./test111.js" defer></script>
    <script>
        console.log('走link後面的內嵌js了')
    </script>
</head>
<body>
    <h1>Hello World!</h1>
</body>
</html>

結果是什麼樣呢?

那要是把defer全部換成async呢?

為什麼會這樣?
因為defer和async都可以使瀏覽器非同步載入並解析執行js檔案,所以link標籤不能阻塞js的檔案的執行,你可能會說,那為什麼defer被阻塞了呢?我們知道defer和async有點不一樣的,在沒有內嵌js時,defer修飾的js會按照DOM先後順序依次執行,async則是先載入完成的先執行;在有內嵌js時,無論是defer還是async都會等待內嵌js執行完才會去執行它們,如果沒有這兩個屬性就會按照DOM中的順序依次執行各個js。

在實際中,我們會使用一些方法來應對css載入阻塞js執行的問題,比如把js放在link之前然後使用DOMContentLoaded方法,或者之前在jQuery中常用的ready方法。

Reference