HTML <script> 指令碼的 async 與 defer 屬性及不同屬性的執行時機與 DOMContentLoaded 事件的關係

钰琪發表於2024-05-01

瀏覽器對於帶有 async, defer 屬性與不攜帶屬性的 <script> 指令碼有不同的行為。
它們可以分別翻譯為:非同步指令碼,延遲指令碼與同步(阻塞)指令碼。
對於模組指令碼,預設是 defer 的行為,它也能設定 async,以更改瀏覽器的處理方式。

同步指令碼

不帶 async 與 defer 屬性的指令碼是同步指令碼,如果它們出現在文件頭部及中間任意位置,會阻塞文件的解析。具體的行為是:

  1. 文件停止解析(與渲染)
  2. 請求指令碼資源
  3. 資源下載完畢後立即解析並執行指令碼
  4. 指令碼解析完畢後恢復文件的解析

為了更好的使用者體驗,對於 HTML 文件,瀏覽器往往是邊下載邊解析邊渲染,而不是等上一項任務完成後再繼續下一項。

如果請求指令碼資源消耗太長時間,或指令碼中有同步的耗時任務,在指令碼執行結束前,文件指令碼位置之後的內容將不被解析與渲染,出現所謂‘白屏’,這會給人不好的感受。

為了保證文件的正常解析與渲染,如果使用同步指令碼,應該將它們放在 <body> 閉合標籤之前,這樣不管指令碼的下載或執行時怎樣的慢,至少文件基本的內容是可讀的。

關於 DOMContentLoaded 事件,它應該在文件解析完成後觸發。但是觸發之前,會等待同步指令碼與延遲指令碼(defer script)執行完畢。

async script (非同步指令碼)

async script 的下載會另起執行緒,與文件的解析是並行的,但它下載完成後會立刻解析並執行指令碼,不論文件是否解析完畢。此時若文件尚未解析完成,則會阻塞文件的解析,當指令碼執行耗時任務,也將會造成‘白屏’。

但是將 async script 放在文件的末尾意義不大,畢竟其下載資源時並不阻塞文件的解析。使用它的目的應該是:

希望指令碼儘快執行,並減少對文件解析的阻塞。

由於 async script 會在下載後立即執行,不能預知哪個指令碼會先下載完成,所以它們的執行時機是未知、無序的。

async script 可能會在文件解析完成之前或之後下載完成並執行,而 DOMContentLoaded 事件的觸發並不會等待 async script 執行完畢,所以並不能知道 async script 會在 DOMContentLoaded 事件觸發之前還是之後執行。基於以上原因,不要在 async script 中依賴別的指令碼,也不要在 async script 中為 DOMContentLoaded 事件註冊回撥,回撥函式能否被呼叫是未知的。

基於非同步指令碼的特點,以下場景適合使用:

  • 無依賴,不操作DOM,且需要儘快執行的任務,使用 async script 來減少對文件解析的阻塞。

    比如:PV/UV埋點統計

  • 在必要的 DOM 載入完成後,儘快為 DOM 元素載入內容、提供互動能力。

    由於 HTML 文件往往是邊下載邊解析邊渲染,可以在網頁首屏的 DOM 載入完畢後引入 async script,操作DOM元素,使使用者可以儘快看到動態載入的內容或與頁面互動。並儘量降低對 HTML 文件解析的影響

    但是除非文件的資料量很可觀,下載與解析要消耗一些時間(3秒以上),否則這麼做往往得不償失。
    一般的動態頁面,文件體積小,解析起來很快(下載到解析完只要幾百毫秒),這麼做不但達不到最佳化目的,阻塞渲染甚至會降低使用者體驗。

使用 async script 的目的是希望指令碼儘快執行,可能是希望提升使用者的使用體驗,也可能是為了避免使用者在指令碼執行前就離開頁面,而錯過什麼必要的任務。

使用它很可能會阻塞文件的解析,一定要慎重考量後做決定。
使用 async 也增加了維護成本:我們要記住它執行時機是不確定的,有些操作在該指令碼中執行可能會無效,還要忍受它要引入依賴就只能在其前面引入同步指令碼。
除此之外,在文件原始碼中間插入 <script> 標籤也並不美觀,它不被渲染到檢視,還破壞了文件的連貫性。

不管出於什麼目的,若決定使用 async script,插入到文件頭部或中間,我們一定要保證指令碼內的任務快速地執行完畢,不要做耗時操作,將對文件解析的阻塞儘量減少。

defer (延遲指令碼)

defer script 會開闢新執行緒下載指令碼,並等待文件解析完畢、所有 defer script 下載完畢後再按指令碼在文件中出現的位置依次執行。
DOMContentLoaded 事件,會等待 defer script 全部執行完畢後才觸發。

defer script 順序載入的特點非常適合一個指令碼依賴於另一個指令碼的情況。
使用 defer script,並將其放到 <head> 中在大多數情況下都是合適的,這麼做和將同步指令碼放到 </body> 之前的效果一模一樣,還能讓 <body> 元素保持整潔,畢竟在尾部放一堆不會被渲染的 <script> 看起來不是那麼的舒服。

相關文章