Javascript - script async vs defer vs end of body

之前和朋友討論在 HTML 中 script tag 的 defer 使用方式,經過仔細的資料查詢與來回討論後,對 defer 的了解更加熟悉,就想說趕緊打鐵趁熱,寫了這篇和大家分享一下。

HTML5  script tag 的定義中談到 script 的運作方式,其中提到了 defer async 屬性(async 是在 HTML5 才有)。asyn 與 defer 其實都是用來讓 script 可以以非同步的方式載入,也就是說 Browser 遇到帶有 async 或 defer 屬性的 script tag 時,會去下載該 script 並同時繼續往下解析其他 HTML DOM,這樣可以減少同步等待 script 下載的問題。下圖是 W3C 定義中解釋原始 script 與 defer, async 的運作流程:


圖中可以看出同步與非同步(defer, async),差異在於同步在遇到下載 script 內容時,Browser 會停下解析 HTML DOM的動作,並等待下載完成後再繼續執行;反之,非同步則是會繼續往下解析

再來看看 defer 與 async 的差異又在哪。在 script 的運作流程中,主要分為下載內容階段與執行階段,在一般的情況下,script 內容下載完成後就會進入執行階段,使用 async 時,就和一般情況一樣,只是是在另一個平行的分支上執行。然後使用 defer 就有點不同了,defer 會先在另一條分支上執行下載動作,但是下載完不執行,直到所有的 DOM 都解析完成後才執行(在 W3C 的定義中說明是在 DOM 解析完後,DOMContentLoaded 事件觸發之前)。

接著下面會來說明各種方式的適用時機與其他注意事項:


async

說明:
async 是在 HTML5 才可使用的 script 屬性,當 Browser 遇到 async 的 script 時會開啟一個分支去進行該 script 的下載與執行階段,如果遇到多個 async script 就會開多個分支。這裡要注意一點,因為每個 script 下載結束時間都會不同,所以如果遇到有相依性的 script 時,有可能發生執行順序錯誤的問題,

適用時機:
  • HTML5
  • script 無相依性
  • script 下載時間長

defer

說明:
defer 與 async 同屬非同步的作法,但差異在於 defer 會在分支中下載 script 內容,直到頁面的 HTML DOM 全部解析完畢後才開始執行(在 DOMContentLoaded 事件觸發之前),不管有多少個 defer script,都會各自下載完才到最後執行,執行的方式就看當時 Browser 遇到的順序。另外 defer 要注意的是,在 script 中不能有 document.write() 出現,因為一遇到 document.write() 就會強迫 Browser 停止解析 HTML DOM,將控制權給該 script。還好如果你用 Google Chrome 的話,他會跑出警告訊息提示你這個 defer script 含有 document.write() 不能執行,警告訊息如下:
Failed to execute 'write' on 'Document': It isn't possible to write into a document from an asynchronously-loaded external script unless it is explicitly opened.


適用時機:
  • script 有相依性
  • script 下載時間長

最後來談一下大家最常用的方式,就是將 script 放在 body 的最末端執行,簡稱 end of body:

end of body

說明:
end of body 的方式是普遍最常用的方式,它採用最原始的 script 執行方式,只是我們強迫自己將 scirpt 擺在 body 區段的最後面,這樣可以確保 Browser 在執行 script 之前都把 HTML DOM 都解析完了,這種方式與 defer 一樣可以確保我頁面上的 DOM 先顯示出來給 User 看,再來在背景執行 script 的動作,讓 User有"頁面已經完成了"的錯覺。end of body 優點在於所有 Browser 通吃,另外也不會影響到 inline script 的運作方式(inline script 就是沒有指定 src 屬性的 script,或者說可認定為 in-page script),在下面補充說明會講到。end of body 有個缺點,就是當 script 載入時間太長的時候,就會拖比較久時間才執行。

適用時機:

  • 任何時機皆可
  • script 下載時間短

最後,下表為 MDN 官網上關於 Browser 支援 Script 的資訊,提供參考決定使用方式:
Feature
Chrome
Edge
Firefox
Internet Explorer
Opera
Safari
Basic support
1.0
(Yes)
1.0 (1.7 or earlier)
(Yes)
(Yes)
(Yes)
async attribute
(Yes)
(Yes)
3.6 (1.9.2)
10
15
(Yes)
defer attribute
(Yes)
(Yes)
3.5 (1.9.1)
10
No support
(Yes)
crossorigin attribute
30.0
(Yes)
13 
No support
12.50
(Yes)
integrity attribute
45.0
No support
43 


No support



補充

前面在 end of body 提到說它不會影響到 inline script,那到底怎樣才會是影響到的呢?

假設我們要引用 jQuery,然後想使用 jQuery 的 document.ready() 的功能,這時候就會在一個 inline script 中使用 document.ready()。這時候我們又想加速 jQuery 的載入速度,所以使用 defer,就會寫成以下方式:
<!DOCTYPE html>
<html>
 <head>
  <title>DeferJSTest</title>
  <link rel="stylesheet" href="style.css" />
  <script
     src="https://code.jquery.com/jquery-3.2.1.min.js"
     integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
     crossorigin="anonymous" defer></script>
  <script type="text/javascript" src="external.js" defer></script>
 </head>
 <body>
  <input type="text" />
  <button id="btn_test" class="btn">My Button</button>
  <script>
   console.log('script executed at the end of body');

   $(function() {
    console.log('script executed after dom load');
   });
  </script>
 </body>
</html>

執行結果:
script executed at the end of body
index.html:18 Uncaught ReferenceError: $ is not defined
    at index.html:18
external.js:1 script executed from external

執行後會發現不管我把 inline script 放到到哪邊(head, body)都會出錯。那是因為 inline script 不支援 defer,所以不管 inline script 放到哪裡都一定會比 defer script 還要早執行。

那究竟要怎麼改呢? 很簡單,前面有說到,defer 是在 DOMContentLoaded 事件觸發前執行,所以我們只要將要在 defer 之後做的事情,放到 DOMContentLoaded 事件裏頭就好了,或者是放到任何比 DOMContentLoaded 事件還要晚觸發的其他事件裡也可以,例如 window.onload 事件。下列為正確版的寫法:
<!DOCTYPE html>
<html>
 <head>
  <title>DeferJSTest</title>
  <link rel="stylesheet" href="style.css" />
  <script
     src="https://code.jquery.com/jquery-3.2.1.min.js"
     integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
     crossorigin="anonymous" defer></script>
  <script type="text/javascript" src="external.js" defer></script>
 </head>
 <body>
  <input type="text" />
  <button id="btn_test" class="btn">My Button</button>
  <script>
   console.log('script executed at the end of body');

   window.addEventListener('DOMContentLoaded', function() {
    $(function() {
     console.log('script executed after dom load');
    });
   })
  </script>
 </body>
</html>

執行結果:
script executed at the end of body
external.js:1 script executed from external
index.html:20 script executed after dom load


進一步了解 script 的各種載入方式後,就可以根據專案的狀態來採取相應的對策,這樣才能做出高效率的網頁。


參考來源:
W3C Recommendation - Scripts
MDN - Scripts
W3C HTML5.2 - Scripting






留言