從一條語句說瀏覽器頁面渲染機制

海寧不想說話發表於2019-02-17

1.引子

最近看到有道題討論document.write在html檔案中不同位置時在頁面上的執行順序(題目在後面具體討論),於是寫這篇部落格討論一下瀏覽器的頁面渲染,在http請求中傳輸的位元組碼如何變成瀏覽器呈現在使用者面前的介面。

2.程式與執行緒

這似乎是個永恆的話題,很多人開始都是混淆的。對於這兩個概念,可以記住下面這幾條:

  • 程式是cpu資源分配的最小單位(系統會給它分配記憶體)
  • 程式之間相互獨立,對於瀏覽器,每開啟一個Tab頁都可以認為開了一個新的程式。
  • 程式擁有自己的多執行緒,各個執行緒之間相互協作完成任務。

在這裡我們只討論一個頁面的渲染機制,即下面說的執行緒都在一個程式內。對於瀏覽器,開一個新頁面(程式)工作的是以下執行緒:

 從一條語句說瀏覽器頁面渲染機制

 與瀏覽器頁面渲染有關的主要是和這裡的GUI引擎執行緒和JS引擎執行緒:

GUI引擎執行緒(後面我們說UI執行緒):解析html css,進行DOMCSSOMRenderTree的繪製,迴流,重繪的執行者,以及頁面渲染都是由他來完成(難以避免的丟擲一大堆概念,後面會一一解釋)。
  • JS引擎執行緒:用來對js檔案進行處理。
  • 上面兩個執行緒是互斥的(請記住這句話,很重要),當有一個在進行時,另外一個將被掛起,也就是說會造成阻塞,至於誰阻塞誰,後面再說。

3.當瀏覽器接收到伺服器發過來的資料包時...

  • 將資料包進行解析。
  • 解析html檔案,UI執行緒進行DOM樹的構建,此操作將確定節點的父子以及兄弟關係。
  • 當繼續解析到類似的<link rel='stylesheet' href='../example.css'/>語句時將下載相應的css檔案,並進行CSSOM的構建(類似DOM的東西),將確定css屬性之間的級聯關係。
  • 瀏覽器將DOMCSSOM進行合併,構建RenderTree,所謂的渲染樹。然後瀏覽器會根據渲染樹進行名為reflow(迴流)的過程,來根據瀏覽器頁面的具體情況確定各個節點的渲染位置(該操作會遍歷整個DOMCSSOM,對效能影響很大)。
  • 接下來就是將準備好的東西渲染到螢幕上了。以上過程可以用下面這張圖演示從一條語句說瀏覽器頁面渲染機制

上面的看似很順暢的過程,卻隱去了一個重大的問題(js呢)?

  • 接上面談起,瀏覽器開始解析時碰到<link rel='stylesheet' href='../example.css'/>會開始下載css檔案,同樣當遇到語句時<script src='./example.js'></script>js引擎會下載並執行js檔案,注意這裡會引起阻塞,阻塞具體情況如下:

    • 阻斷DOM的構建,應為瀏覽器不知道js檔案會對DOM進行什麼操作,也就是說JS執行會阻塞DOM構建
    • 如果CSSOM沒有就緒,那麼JS將等到CSSOM準備就緒時再執行,也就是說CSSOM的構建會阻塞JS執行,其實也就是間接的在阻塞DOM的構建
  • 那麼我們是否能對JS的執行進行操作呢,答案是:可以.
    • async屬性<script src='./example.js' async></script>
      • 它的作用是指定相應的js檔案在下載好再進行執行,也就是說這個js檔案在下載過程中是不阻塞DOM構建的。
    • defer屬性<script src='./example.js' defer></script>
      • 指定對應js檔案在整個頁面都解析完成後再執行,此時DOMCSSOM都已經準備就緒。

說一組概念(很重要):

  • 迴流(reflow)和重繪(repaint)
    • 迴流:當Render Tree中部分或全部元素的尺寸、結構、或某些屬性發生改變時,瀏覽器重新渲染部分或全部文件。
    • 重繪:當頁面中元素樣式的改變並不影響它在文件流中的位置時,瀏覽器會將新樣式賦予給元素並重新繪製它。
    • 至於具體什麼操作會引起迴流以及重繪,這裡不一一列出。
    • 迴流必定重繪,重繪不一定迴流。
    • 很明顯:迴流的代價要比重繪高很多。在效能優化時有一點就是避免頻繁造成迴流。

4.那麼回到我們開篇的問題

先來介紹這條語句:

  • document.write
將一個文字字串寫入由 document.open() 開啟的一個文件流。
注意: 因為 document.write 需要向文件流中寫入內容,因此在關閉(已載入)的文件上呼叫 document.write 會自動呼叫 document.open這將清空該文件的內容

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script>
        document.write('指令碼輸出');
    </script>
</head>
<body>
    <p>頁面內容</p>
    <p>頁面內容</p>
</body>
</html>複製程式碼

頁面輸出結果:

//指令碼輸出
//頁面內容
//頁面內容複製程式碼

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <p>頁面內容</p>
    <script>document.write('指令碼輸出');</script>    <p>頁面內容</p>
</body>
</html>複製程式碼

頁面輸出結果:

//頁面內容
//指令碼輸出
//頁面內容複製程式碼

以上的結果看似都是情理之中,document.write會在執行到時將內容新增到DOM樹中。

而當我們把這條語句放到</body>或者</html>之後時,瀏覽器的做法都是將這條語句提升到</body>之前執行,下面是谷歌還有火狐,IE的截圖。

  • chrome從一條語句說瀏覽器頁面渲染機制


  • firefox從一條語句說瀏覽器頁面渲染機制


  • IE從一條語句說瀏覽器頁面渲染機制


還有就是將上面的document.write放在onload時執行,像下面這樣:

<!DOCTYPE html><html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script>
        window.onload=function(){
            document.write('指令碼輸出');
        }
    </script>
</head>
<body>
    <p>頁面內容</p>
    <p>頁面內容</p>
</body>
</html>複製程式碼

結果就是,不論script標籤在哪,頁面上都只會渲染指令碼輸出一句話,所以我們得到下面結論:

  • 瀏覽器解析DOMbody標籤為止
  • body標籤之後的script會被提升到</body>之前執行,但仍在onload事件之前。

後記:

根據個人理解以及整理得,有錯誤或者偏差敬請原諒,歡迎指正。


相關文章