一個簡單的例子瞭解async跟defer

不吃貓的魚發表於2019-02-28

script標籤

當我們要在頁面當中引入指令碼的時候,一般用的是script標籤(i.e. <script>)。很多人對script標籤的第一印象就是- 阻塞。在High Performance Web Sites 雅虎軍規第6條中也提到儘量把script指令碼放在body尾部。通過一個小例子我們看看script放在不同位置所產生的不同效果最後得出一丟丟優化結論。

首先明確一點,當在html頁面裡引入script時,瀏覽器做了2件事情:

  • 獲取/載入指令碼內容,這部分不會阻塞!
  • 執行獲取的指令碼內容,會阻塞!

假設我們有2個指令碼:

//script1.js
let t1 = +new Date;
console.log(`script1 is loading at`, t1);

console.log(`script1 element`, document.getElementById(`load-experiment`));
while((+new Date) - t1 < 1000) {
    // delay 1 seconds
}
console.log(`script1 finishes loading at`, +new Date);複製程式碼
//script2.js
let t2 = +new Date;
console.log(`script2 is loading at`, t2);

console.log(`script2 element`, document.getElementById(`load-experiment`));
while((+new Date) - t2 < 2000) {
    // delay 2 seconds
}
console.log(`script2 finishes loading at`, +new Date);複製程式碼

把script標籤都放head裡

<!--all-in-head.html-->
<html>
    <head>
        <title> test js tag async and defer attributes</title>
        <script src=`./script1.js`></script>
        <script src=`./script2.js`></script>
    </head>
    <body>
        <h1 id=`load-experiment`> hello world </h1>
    </body>
</html>複製程式碼

console裡的輸出為:

script1 is loading at 1496747869008
script1.js:4 script1 element null
script1.js:8 script1 finishes loading at 1496747870008
script2.js:2 script2 is loading at 1496747870009
script2.js:4 script2 element null
script2.js:8 script2 finishes loading at 1496747872009複製程式碼

發現:

  • 用瀏覽器開啟這個html會發現有明顯的空白時間。這是因為script1跟script2的 執行 阻塞了DOM的載入。
  • script指令碼執行的時候並不能獲取DOM元素(getElementById返回null)。驗證了上一條
  • script之間也是相互阻塞的。script2是等script1執行好後才執行。

把script標籤都放body尾部

這也是雅虎軍規High Performance Web Sites第6條裡的建議。

<!--all-in-body.html-->
<html>
    <head>
        <title> test js tag async and defer attributes</title>
    </head>
    <body>
        <h1 id=`load-experiment`> hello world </h1>
        <script src=`./script1.js`></script>
        <script src=`./script2.js`></script>
    </body>
</html>複製程式碼

console裡的輸出為:

script1 is loading at 1496751597679
script1.js:4 script1 element <h1 id=​"load-experiment">​ hello world ​</h1>​
script1.js:8 script1 finishes loading at 1496751598679
script2.js:2 script2 is loading at 1496751598680
script2.js:4 script2 element <h1 id=​"load-experiment">​ hello world ​</h1>​
script2.js:8 script2 finishes loading at 1496751600680複製程式碼

發現:

  • 用瀏覽器開啟這個html不會有明顯的空白時間。這是因為script1跟script2的發生在DOM載入後。
  • script指令碼執行的時候能獲取DOM元素(getElementById返回節點)。驗證了上一條
  • script之間是相互阻塞的。script2是等script1執行好後才執行。

把所有的script指令碼放body尾部的情況過於理想化了。例如body裡有些內聯JS需要引用到的功能程式碼就需要放head裡提前執行好。最常見的例子就是計算首屏載入時間(Above The Fold),首屏載入時間肯定是不能等DOM載入完。這時候一般是在head里載入功能庫並觸發首屏載入開始計時,然後在首屏的末尾處用內聯JS觸發首屏載入結束計時。這時候就需要有些程式碼是在head裡的。

script標籤相應地放head跟body裡

根據需要把script指令碼相應地放在head跟body裡。這是最常見的情況。

<html> 
    <head> 
        <script src="headScripts.js"></scripts> 
    </head> 
    <body>
        <h1 id=`load-experiment`> hello world </h1>
        <script src="bodyScripts.js"></script> 
    </body>
</html>複製程式碼

defer!

把script放body尾的話有個缺點就是要等DOM載入好了才會去載入並執行script指令碼。前面開頭的時候說過獲取/載入指令碼內容不會阻塞,只有執行指令碼內容的時候才會阻塞!如果能在載入DOM的同時載入script指令碼,等到DOM解析好了再執行指令碼,這樣就可以節省部分載入指令碼的時間。當指令碼比較大時提高的效率會很可觀。於是就有了script標籤裡的defer屬性: 並行載入該指令碼,直到DOM解析完了並且它前面含有defer的指令碼都執行完後,才可以執行該指令碼。

<!--defer.html-->
<html>
    <head>
        <title> test js tag async and defer attributes</title>
        <script defer src=`./script1.js`></script>
        <script defer src=`./script2.js`></script>
    </head>
    <body>
        <h1 id=`load-experiment`> hello world </h1>
    </body>
</html>複製程式碼

console裡的輸出為:

script1 is loading at 1496760312686
script1.js:4 script1 element <h1 id=​"load-experiment">​ hello world ​</h1>​
script1.js:8 script1 finishes loading at 1496760313686
script2.js:2 script2 is loading at 1496760313686
script2.js:4 script2 element <h1 id=​"load-experiment">​ hello world ​</h1>​
script2.js:8 script2 finishes loading at 1496760315686複製程式碼

發現:

  • 跟把script放body的情況輸出一樣。從可以獲取到DOM元素可以看出它會等到DOM解析好再執行。
  • 新增了defer屬性的指令碼是會按順序執行的。script1先於script2執行。

async!

通常頁面上會有些附加功能元件。這種元件相互獨立並且不是該頁面必需元件。也就是說該種指令碼無執行順序要求(因為獨立)並且就算載入或者執行失敗也不會對頁面造成致命影響的。例如頁面上的評論功能跟聊天功能。

<!--async.html-->
<html>
    <head>
        <title> test js tag async and defer attributes</title>
    </head>
    <body>
        <h1 id=`load-experiment`> hello world </h1>
        <script async src=`./script1.js`></script>
        <script async src=`./script2.js`></script>
    </body>
</html>複製程式碼

多次重新整理頁面可以看到每次輸出結果不一樣。體現在:

  • scrip1跟script2的執行順序在變
  • 獲取DOM元素的結果在變。有時能獲取到有時獲取不到

頁面上加了async的指令碼無法保證其執行順序,這通常也是隱藏bug的來源。除非你確定該指令碼跟頁面其他指令碼無依賴關係,不然慎用async吧。

總結

綜上所述,最後引入指令碼的形式大概是這樣滴:

<html> 
    <head> 
        <!--headScripts.js為必須在DOM解析前載入並執行的指令碼-->
        <script src="headScripts.js"></scripts> 
        <!--bodyScripts.js加個defer,先載入。等DOM解析好了再執行-->
        <script defer src="bodyScripts.js"></script> 
    </head> 
    <body>
        <!--body內容-->
        <h1 id=`load-experiment`> hello world </h1>
        <!--獨立的元件,不依賴DOM, 錦上添花型的,可用async-->
        <script async src="forumWidget.js"></script>
        <script async src="chatWidget.js"></script>
    </body>
</html>複製程式碼

程式碼示例

Reference

Notice

  • 如果您覺得該Repo讓您有所收穫,請「Star 」支援樓主。
  • 如果您想持續關注樓主的最新系列文章,請「Watch」訂閱

相關文章