首頁 > 軟體

網頁資源阻塞瀏覽器載入的原理範例解析

2023-03-09 06:04:57

正文

一個頁面允許載入的外部資源有很多,常見的有指令碼、樣式、字型、圖片和視訊等,對於這些外部資源究竟是如何影響整個頁面的載入和渲染的呢?今天來一探究竟。

  • 如何用 Chrome 客製化網路載入速度?
  • 圖片/視訊/字型會阻塞頁面載入嗎?
  • CSS 是如何阻塞頁面載入的?
  • JS 又是如何阻塞頁面載入的?
  • JS 一定會阻塞 DOM 載入嗎?
  • defer 和 async 是什麼?又有何特點?
  • 動態指令碼會造成阻塞嗎?
  • 阻塞是怎麼和 DOMContentLoaded 與 onload 扯上關係的?

測試前環境準備

測試之前需要對瀏覽器下載資源的速度進行控制,將它重新設定為 50kb/s,操作方式:

  • 開啟 Chrome 開發者工具;
  • 在 Network 面板下找到 Disable cache 右側的下拉選單,然後選擇 Add 新增自定義節流設定;
  • 新增一個下載速度為 50kb/s 的設定;
  • 最後在第二步驟中的下拉選單選擇剛剛設定的選項即可;
  • 注意:如果當前選擇的自定義選項被修改了,則需要切換到別的選項再切回來才可生效。

為什麼是這個速度?因為如下的一些資源,比如圖片、樣式或者指令碼體積都是 50kb 的好幾倍,方便測試。

圖片會造成阻塞嗎?

直接寫個範例來看下結果:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <script>
        document.addEventListener('DOMContentLoaded', () => {
            console.log('DOMContentLoaded')
        })
        window.onload = function() {
            console.log('onload')
        }
    </script>
</head>
<body>
    <h1>我是 h1 標籤</h1>
    <img src="https://xxx.oss-cn-shenzhen.aliyuncs.com/images/flow.png" />
    <h2>我是 h2 標籤</h2>
</body>
</html>

上面這張圖片的大小大概是 200kb,當把網路下載速度限制成 50kb/s,開啟該頁面,可以看到如下結果:當 h1h2 標籤渲染出來且列印了 DOMContentLoaded 的時候,此時圖片還在載入中,這就說明了圖片並不會阻塞 DOM 的載入,更加不會阻塞頁面渲染;當圖片載入完成的時候,會列印 onload,說明圖片延遲了 onload 事件的觸發。
視訊、字型和圖片其實是一樣的,也不會阻塞 DOM 的載入和渲染。

CSS 載入阻塞

同樣的,還是直接用程式碼來測試 CSS 載入對頁面阻塞的情況,因為下面程式碼載入的 bootstrap.css 是 192kb 的,所以理論上下載它應該需要花費 3 到 4 秒左右。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="stylesheet" />
</head>
<body>
    <h1>我是 h1 標籤</h1>
</body>
</html>

測試過程如下:

  • Elements 面板下,選中 h1 這個標籤,然後按 delete 鍵將它從 DOM 中刪掉,從而模擬首次載入;
  • 重新整理瀏覽器,馬上 Elements 面板下就載入出 h1 標籤,繼續載入 3 到 4 秒後(此時正在載入 bootstrap.css),頁面出現 我是 h1 標籤 字樣,此時頁面已經渲染完成。

從而得出結論:

  • bootstrap.css 還沒載入完成,而 DOM 中就已經出現 h1 標籤,說明 CSS 不會阻塞 DOM 的解析;
  • 頁面直到 bootstrap.css 載入完成才出現 h1 裡的文案,說明 CSS 會阻塞 DOM 的渲染。

為什麼是這個結論呢?試想一下頁面渲染的流程就知道了。瀏覽器首先解析 HTML 生成 DOM 樹,解析 CSS 生成 CSSOM 樹,然後 DOM 樹和 CSSOM 樹進行合成生成渲染樹,通過渲染樹進行佈局並且計算每個節點資訊,繪製頁面。


可以說解析 DOM 和 解析 CSS 其實是並列進行的,既然是並列進行的,那 CSS 和 DOM 就不會互相影響了,這和結論一相符;另外渲染頁面一定是在得到 CSSOM 樹之後進行的,這和結論二相符。
CSS 一定會阻塞 DOM 的渲染嘛?答案是否定的,當把外連樣式放到 <body> 最尾部去載入:

<body>
    <h1>我是 h1 標籤</h1>
    <link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="stylesheet" />
</body>

此時重新整理瀏覽器,頁面上會馬上顯示出 我是 h1 標籤 字樣,當 3 到 4 秒過後樣式載入完成的時會造成二次渲染,頁面重新渲染出該字樣,這就說明 CSS 阻塞 DOM 的渲染只阻塞定義在 CSS 後面的 DOM。二次渲染會對使用者造成不好的體驗且加重了瀏覽器的負擔,所以這也就是為什麼需要把外連樣式提前到 <head> 裡載入的原因。

CSS 會阻塞後面 JS 的執行嗎?

CSS 阻塞了後面 DOM 的渲染,那它會阻塞 JS 的執行嘛?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="stylesheet" />
</head>
<body>
    <h1>我是 h1 標籤</h1>
    <script>
        console.log('888')
    </script>
</body>
</html>

重新整理瀏覽器的時候可以看到,瀏覽器 Console 面板下沒有列印內容,而當樣式載入完成的時候列印了 888,這就說明 CSS 會阻塞定義在其之後 JS 的執行。

為什麼會這樣呢?試想一下,如果 JS 裡執行的操作需要獲取當前 h1 標籤的樣式,而由於樣式沒載入完成,所以就無法得到想要的結果,從而證明了 CSS 需要阻塞定義在其之後 JS 的執行。

JS 載入阻塞

CSS 會阻塞 DOM 的渲染和阻塞定義在其之後的 JS 的執行,那 JS 載入會對渲染過程造成什麼影響呢?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <script src="https://cdn.bootcss.com/jquery/2.1.4/jquery.min.js"></script>
</head>
<body>
    <h1>我是 h1 標籤</h1>
</body>
</html>

首先刪除頁面中已經存在的 h1 標籤(如果存在的話),仔細觀察 Elements 面板,當重新整理瀏覽器的時候,一直未載入出 h1 標籤(期間頁面一直白屏),直到 JS 載入完成後,DOM 中才出現,這足以說明了 JS 會阻塞定義在其之後的 DOM 的載入,所以應該將外部 JS 放到 <body> 的最尾部去載入,減少頁面載入白屏時間。

defer 和 async

JS 一定會阻塞定義在其之後的 DOM 的載入嗎?來測試一下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <script async src="https://cdn.bootcss.com/jquery/2.1.4/jquery.min.js"></script>
</head>
<body>
    <h1>我是 h1 標籤</h1>
</body>
</html>

上面這段程式碼的測試結果是當頁面中顯示出 h1 標籤的時候,指令碼還沒有載入完成,這就說明了 async 指令碼不會阻塞 DOM 的載入;同理可以用同樣的方式測試 defer,也會得到這個結論。

現在知道了通過 defer 或者 async 方式載入 JS 的時候,它是不會阻塞 DOM 載入的。知道 deferasync 是什麼嗎?它們兩者有什麼區別呢?

回答這些疑問之前,先來看下當瀏覽器解析 HTML 遇到 script 標籤的時候會發生什麼?

  • 暫停解析 DOM;
  • 執行 script 裡的指令碼,如果該 script 是外連,則會先下載它,下載完成後立刻執行;
  • 執行完成後繼續解析剩餘 DOM。

上面這是解析時遇到一個正常的外連的情況,正常外連的下載和執行都會阻塞頁面解析;而如果外連是通過 defer 或者 async 載入的時候又會是如何呢?

defer 特點

  • 對於 deferscript,瀏覽器會繼續解析 html,且同時並行下載指令碼,等 DOM 構建完成後,才會開始執行指令碼,所以它不會造成阻塞;
  • defer 指令碼下載完成後,執行時間一定是 DOMContentLoaded 事件觸發之前執行;
  • 多個 defer 的指令碼執行順序嚴格按照定義順序進行,而不是先下載好的先執行;

async 特點

  • 對於 asyncscript,瀏覽器會繼續解析 html,且同時並行下載指令碼,一旦指令碼下載完成會立刻執行;和 defer 一樣,它在下載的時候也不會造成阻塞,但是如果它下載完成後 DOM 還沒解析完成,則執行指令碼的時候是會阻塞解析的;
  • async 指令碼的執行 和 DOMContentLoaded 的觸發順序無法明確誰先誰後,因為指令碼可能在 DOM 構建完成時還沒下載完,也可能早就下載好了;
  • 多個 async,按照誰先下載完成誰先執行的原則進行,所以當它們之間有順序依賴的時候特別容易出錯。 :::info deferasync 都只能用於外部指令碼,如果 script 沒有 src 屬性,則會忽略它們。 :::

動態指令碼會造成阻塞嗎?

對於如下這段程式碼,當重新整理瀏覽器的時候會發現頁面上馬上顯示出 我是 h1 標籤,而過幾秒後才載入完動態插入的指令碼,所以可以得出結論:動態插入的指令碼不會阻塞頁面解析。

<!-- 省略了部分內容 -->
<script>
    function loadScript(src) {
        let script = document.createElement('script')
        script.src = src
        document.body.append(script)
    }
    loadScript('https://cdn.bootcss.com/jquery/2.1.4/jquery.min.js')
</script>
<h1>我是 h1 標籤</h1>

動態插入的指令碼在載入完成後會立即執行,這和 async 一致,所以如果需要保證多個插入的動態指令碼的執行順序,則可以設定 script.async = false,此時動態指令碼的執行順序將按照插入順序執行和 defer 一樣。

DOMContentLoaded 和 onload

在瀏覽器中載入資源涉及到 2 個事件,分別是 DOMContentLoadedonload,那麼它們之間有什麼區別呢?

  • onload:當頁面所有資源(包括 CSS、JS、圖片、字型、視訊等)都載入完成才觸發,而且它是繫結到 window 物件上;
  • DOMContentLoaded:當 HTML 已經完成解析,並且構建出了 DOM,但此時外部資源比如樣式和指令碼可能還沒載入完成,並且該事件需要繫結到 document 物件上;

一定看到了上面的可能二字,為什麼當 DOMContentLoaded 觸發的時候樣式和指令碼是可能還沒載入完成呢?

DOMContentLoaded 遇到指令碼

當瀏覽器處理一個 HTML 檔案,並在檔案中遇到 <script> 標籤時,就會在繼續構建 DOM 之前執行它。這是一種防範措施,因為指令碼可能想要修改 DOM,甚至對其執行 document.write 操作,所以 DOMContentLoaded 必須等待指令碼執行結束後才觸發。以下這段程式碼驗證了這個結論:當指令碼載入完成的時候,Console 面板下才會列印出 DOMContentLoaded

<script>
    document.addEventListener('DOMContentLoaded', () => {
        console.log('DOMContentLoaded')
    })
</script>
<h1>我是 h1 標籤</h1>
<script src="https://cdn.bootcss.com/jquery/2.1.4/jquery.min.js"></script>

那麼一定是指令碼執行完成後才會觸發 DOMContentLoaded 嘛?答案也是否定的,有兩個例外,對於 async 指令碼和動態指令碼是不會阻塞 DOMContentLoaded 觸發的。

DOMContentLoaded 遇到樣式

前面已經介紹到 CSS 是不會阻塞 DOM 的解析的,所以理論上 DOMContentLoaded 應該不會等到外部樣式的載入完成後才觸發,這麼分析是對的,用下面程式碼進行測試一翻就知道了:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <script>
        document.addEventListener('DOMContentLoaded', () => {
        console.log('DOMContentLoaded')
    })
    </script>
    <link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="stylesheet"/>
</head>
<body>
    <h1>我是 h1 標籤</h1>
</body>
</html>

測試結果:當樣式還沒載入完成的時候,就已經列印出 DOMContentLoaded,這和分析的結果是一致的。但是一定是這樣嘛?顯然不一定,這裡有個小坑,(基於上面程式碼)在樣式後面再加上 <script> 標籤的時候,會發現只有等樣式載入完成了才會列印出 DOMContentLoaded,為什麼會這樣呢?正是因為 <script> 會阻塞 DOMContentLoaded 的觸發,所以當外部樣式後面有指令碼(**async**** 指令碼和動態指令碼除外)的時候,外部樣式就會阻塞 **DOMContentLoaded** 的觸發**。

<head>
<!-- 只顯示了部分內容 -->
<link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="stylesheet"/>
<script></script>
</head>

以上就是網頁資源阻塞瀏覽器載入的原理範例解析的詳細內容,更多關於網頁資源阻塞瀏覽器載入的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com