script tag 的 async 和 defer 屬性


script標籤

針對 <script> 標籤放哪裡,一般會有兩種版本:

  • 放在 <head> ... </head> 之間
  • 放在 </body> 之前

<script> 標籤放在 </body> 之前,網頁可以正常運作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
</head>

<body>
<h1 id="title"></h1>
<script>
document.querySelector('#title').textContent = "這是標題"
</script>
</body>

</html>

接著,我們試著把 <script> 標籤移到 <head> ... </head> 之間,這時候會發現網頁一片空白:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">

<script>
document.querySelector('#title').textContent = "這是標題"
</script>
</head>

<body>
<h1 id="title"></h1>

</body>

</html>

圖片來源:Asynchronous vs Deferred JavaScript



瀏覽器解析 HTML 是一行一行依序向下讀取,在傳統的寫法中,當瀏覽器讀到 <script> 時,便會 暫停解析 DOM,立刻開始下載 <script> 的資源,並在下載完成後立刻執行。由於這樣的特性,便可能造成在 DOM 樹建構不完全時就執行 JavaScript,其中需要操作 DOM 的程式可能就因此無法正確運作,許多衍伸的問題也就因此產生;若是透過 src 外聯 .js 檔案,瀏覽器會「同步地」下載 .js 檔案,在下載完成並執行完程式碼之前,後續的其他資源下載、頁面剖析等,都會被阻斷。執行過程中,使用者便會卡在白畫面,並產生覺得網站太慢、使用者體驗不好等感受。

不過,文件資源的完整載入,是指 HTMLCSS 、圖片等都載入完成,而不單指 DOM樹 建立完成;若想在文件剖析完成、 DOM樹 生成時就執行程式碼,建議是將 script 放在文件尾端,通常是 body 標籤之前,因為此時 DOM樹 已經建立,操作 DOM 節點就不是問題了。但在更複雜的網站中,HTMLJavaScript 的檔案都來越大,下載、執行時間也越來越長,需要等到整個 DOM 樹都載入完成才開始下載 <script> 內的資源,從網站讀取完成到可操作之間便會有明顯的延遲感。


這樣的問題該怎麼解決呢?

async & defer

從 HTML4 開始,<script> 便多了 defer 屬性,HTML5 則多了 async,兩者皆是用來幫助開發者控制 <script> 內資源的載入及執行順序,以及避免 DOM 的解析被資源下載卡住。這兩個屬性只適用在外部引入的檔案,對內嵌程式碼的script標籤沒有影響。

defer

defer 意旨為 延遲(Deferred),也就是說,加上 defer 屬性後,瀏覽器會繼續解析、渲染畫面,而不會因為需要載入 <script> 內的資源而卡在那邊等;如果有多個設置 deferjs 標籤存在,則會由上到下依照擺放順序觸發。實際上的執行時間,會在 DOM渲染完畢後,DOMContentLoaded 事件執行之前。

1
<script defer src="script.js">/script>

圖片來源:Asynchronous vs Deferred JavaScript



defer 屬性告訴瀏覽器在 HTML 還在解析時加載 js,但是等到 HTML 整個解析完才執行 js。

async

async 即為 非同步(Asynchronous),在 <script> 加上 async 屬性後,與 defer 相同的是會在背景執行下載,但不同的是當下載完成會馬上暫停 DOM 解析(若尚未解析完的話),並開始執行 JavaScript。也因為下載完成後會立即執行,如果有多個 async 屬性的 js ,先下載完的就會先執行,因為下載完成的順序是無法預測的,因此「執行順序也就無法預測」。

1
<script async src="script.js"></script>

圖片來源:Asynchronous vs Deferred JavaScript



async 屬性告訴瀏覽器可以異步執行(executed asynchronously),在 HTML 還在解析時加載 js ,當 js 完全下載後才暫停解析 HTML ,執行 js

type=”module”

在主流的現代瀏覽器中,<script> 的屬性可以加上 type="module"。這時,瀏覽器會將此檔案認為是一個 JavaScript 模組,其中的解析規則、執行環境會略有不同;這時候的 <script> 預設行為會像是 defer 一樣,背景下載,且等待 DOM 解析、渲染完成後才執行,也因此 defer 屬性無法在 type="module" 產生作用。但同樣可以透過 async 屬性讓它在下載完成後即刻執行。

使用場景

前面已經針對這兩個屬性進行說明了,那麼該如何正確地使用呢?

defer 由於背景載入、不打斷渲染及確保執行順序的特色,基本上沒特別需求的話,建議設定在 <script> ;當然,<script> 本身的擺放順序還是要稍微留心注意。
async 比較特別,因為下載後會立刻執行,且不保證順序,一般常見的應用是設定在完全獨立的小模組,例如 背景 Log、頁面廣告等等,在避免造成使用者體驗變差的同時,盡量提早開始產生效果。

asyncdefer 是專屬於 <script> 的屬性,而網頁中的其他資源,我們可以透過 <link>preloadprefetch 屬性,來幫我們 延遲載入 未來才需要用到的資源,詳細的請參考 Preload vs Prefetch

參考文獻

  1. https://bitsofco.de/async-vs-defer/

  2. https://ithelp.ithome.com.tw/articles/10216858?source=post_page—–8205fddbbafc———————-

  3. https://ithelp.ithome.com.tw/articles/10208563

  4. https://ithome.com.tw/voice/132470

  5. https://www.cnblogs.com/jiasm/p/7683930.html

  6. https://kknews.cc/code/4eoxg4v.html