Skip to content

一般、async 與 defer script 在 HTML 解析、下載與執行時機上的差異

script 中 async、defer 與 module 的差異 ​

前面已經知道 HTML parser 遇到 script 時,下載與執行時機可能會影響 DOM 建立和畫面顯示。不過我們常說的「載入 JavaScript」,其實還可以拆成 fetch、parse、compile 與 execute 等工作;async、defer 和 module 主要是在改變資源取得與執行如何配合 HTML parsing,不代表 JavaScript 下載完成後就已經執行完成。

這篇會先用帶有 src 的外部 classic script,比較不加屬性、加上 async 與加上 defer 的差異,再補上 type="module" 的預設行為。每種方式適合的情境都不太一樣,不能無腦使用喔!

預設 script ​

當 HTML parser 讀到沒有 async、defer 或 type="module" 的外部 script 時,會先暫停解析 HTML,等待 script 下載並執行完成後,才繼續解析後面的內容。

如果把這種 script 放在 <head> 裡面,可能會延後後面的頁面內容出現在畫面上的時間。通常使用者一覺得等待太久,就會想離開當前頁面,所以還是要依照情境使用!

寫法上就會是這樣:

html
<script src="script.js"></script>

雖然會阻塞 HTML 解析,但還是有適合的使用場景。比方說為了避免頁面先顯示淺色、再突然跳成深色,通常會在瀏覽器開始呈現頁面前,透過一小段 inline script 同步套用正確的主題。這時候刻意利用阻塞行為,就能避免主題閃爍。

一般 script 會等下載與執行完成後才繼續解析 HTML

async script ​

async script 可以在 HTML 解析期間並行下載,下載完成後就會盡快執行。如果當下 HTML 還在解析,執行 script 時仍會暫停解析;而且多支 async script 會依照各自準備完成的時間執行,不保證文件中的宣告順序。

所以,當 script 需要固定的執行順序時,通常不建議使用 async。它比較適合不必等 DOM 解析完成,也不依賴其他 script 的獨立功能,例如 Google Analytics 或其他行為偵測工具。因為使用者可能在兩秒內操作完畫面就關閉頁面;如果追蹤工具直到五秒後才完成下載與初始化,前面的操作就可能來不及被記錄。因此,這類工具通常會希望在 HTML 解析期間下載,並在資源準備好後盡快執行,Google 官方提供的 tag 範例就使用 async。

不過,async 在這裡能做的,只是讓 script 不必等待 HTML 解析完成便執行;實際上能不能記錄到使用者離開前的操作,還是會受到網路速度、tag 初始化、事件送出時機及使用者同意狀態等因素影響,不是加上 async 就一定不會漏掉。

寫法上就會像下面這樣:

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

除了直接寫在 HTML 裡,也可以用 JavaScript 動態產生外部 script。這類 script 預設會採用 async 的執行方式;如果多支動態 script 之間需要維持順序,就要在插入文件前明確設定 script.async = false:

javascript
const sources = ['/vendor.js', '/app.js']

for (const source of sources) {
  const script = document.createElement('script')
  script.async = false
  script.src = source
  document.body.append(script)
}

這裡要特別注意,script.async = false 不代表動態 script 會變成預設的 parser-blocking script。只有由 HTML parser 解析出來,而且沒有 async、defer 或 type="module" 的 classic script,才會中斷 HTML 解析。透過 document.createElement() 建立的 script 並不是 parser-inserted script,設定 async = false 只會讓多支 script 依照插入順序執行;它們仍會在符合順序且資源準備好後盡快執行,不會像 defer 一樣等待 HTML 解析完成。

async script 與 HTML 並行下載,下載後暫停解析並執行

defer script ​

defer script 可以在 HTML 解析期間並行下載,但會等文件解析完成後才執行。多支 defer script 會依照文件中的宣告順序執行,而且都會在 DOMContentLoaded 事件觸發前執行完成。

有了這個特性後,defer 就很適合需要操作 DOM,或彼此有固定執行順序的 script。比方說,要先載入 Vue,接著才能執行依賴 Vue 的第三方套件,就很適合這樣做!

html
<script defer src="vue.js"></script>
<script defer src="plugin.js"></script>

defer script 與 HTML 並行下載,解析完成後依序執行

這裡要注意,defer 只對帶有 src 的 classic script 有效果;把 defer 加在 inline classic script 上不會得到相同的延後行為。

module script ​

<script type="module"> 會使用 JavaScript module 的載入規則。外部 module script 與 HTML parsing 並行下載,並遞迴取得它 import 的 dependencies;在沒有 async 時,預設行為和 defer 類似,會等文件解析完成後才執行,而且 DOMContentLoaded 也會等待 module 執行完成。

html
<script type="module" src="/assets/app.js"></script>

Inline module 也會延後到文件解析完成後執行,不需要額外加上 defer:

html
<script type="module">
  import { mountApp } from '/assets/app.js'

  mountApp()
</script>

Module script 還有幾個跟 classic script 不一樣的地方:它預設使用 strict mode、同一個 module 在同一份 document 中只會 evaluate 一次,而且跨來源取得 module 時會套用 CORS。假如在 module script 上再加上 async,就不會等待 HTML 解析完成,也不保證多支 module script 依文件順序執行;整個 dependency graph 準備好後便可以盡快執行。

html
<script async type="module" src="/analytics.js"></script>

所以 module 並不是把 classic script 換個副檔名而已。除了下載時機,還要一起考慮 dependency graph、CORS 與執行順序。

總結 ​

如果先把 script 的載入簡化成「取得」與「執行」兩個階段,幾種寫法的差異就會清楚很多:

寫法取得時機執行時機是否中斷 HTML 解析DOMContentLoaded 是否等待多支 script 的執行順序
預設 classic script解析到標籤時等待下載下載完成後立即執行下載與執行都會阻塞解析會等它完成依文件中的宣告順序
async classic script與 HTML 解析並行下載下載完成後盡快執行執行時仍會中斷解析不會特別等待不保證順序
defer classic script與 HTML 解析並行下載HTML 解析完成後執行不會在解析途中阻塞會等待依文件中的宣告順序
module script與 HTML 解析並行取得 dependency graphHTML 解析完成後執行不會在解析途中阻塞會等待沒有 async 時依文件順序處理
async module script與 HTML 解析並行取得 dependency graph準備完成後盡快執行執行時可能中斷解析不會特別等待不保證順序

簡單來說,如果 script 完全獨立、不依賴 DOM 解析完成或其他 script,而且希望準備完成後盡快執行,可以考慮使用 async;如果 classic script 需要操作 DOM,或彼此之間有固定的執行順序,defer 通常會是比較穩定的選擇。使用 ESM 時,module script 本身就有 defer-like 行為,不需要再加 defer。只有在 script 必須先執行完成,後面的 HTML 才能繼續解析時,才需要使用預設的阻塞行為。

另外,透過 JavaScript 動態插入的外部 classic script 預設會採用 async 的執行方式。如果多支動態 script 之間需要維持順序,就要在插入文件前明確設定 script.async = false。

Script 要在什麼時間執行是一件事,執行後會不會長時間占住 Main Thread 又是另一件事。後續談到 Event Loop 與 Long Task 時,會再接著看 JavaScript 為什麼可能延後輸入事件與下一次畫面更新。

參考資料 ​

按 Enter 或空白鍵放大圖片,按 Escape 關閉放大檢視。