前端從超文本標記語言(HTML, HyperText Markup Language)出發,本章討論 <script> tag 加上 asyncdefer 的功能及差異。

講古#

瀏覽器解析 HTML 是一行一行依序向下讀取。傳統寫法中,當瀏覽器讀到 <script>,會暫停解析文件物件模型(DOM, Document Object Model),立刻下載 <script> 資源,並在下載完成後執行。

這帶來兩個問題:

  • DOM 樹建構不完全時就執行 JavaScript,操作 DOM 的程式可能無法正確運作
  • 等待 <script> 下載、執行的過程,使用者卡在白畫面,覺得網站太慢

當時的解決方法簡單直白:把 <script> 放到 <body> 的最後一行,避免 DOM 樹未建構完就執行程式。

但隨著網站變複雜,HTML、JavaScript 檔案越來越大,下載、執行時間越拉越長。需要等到整個 DOM 樹載入完成才開始下載 <script> 資源,從網站讀取完成到可操作之間便有明顯的延遲感。

這樣的問題該怎麼解決?

簡介#

從 HTML4 開始,<script> 便多了 defer 屬性,HTML5 則多了 async,兩者皆是用來幫助開發者控制 <script> 內資源的載入及執行順序,以及避免 DOM 的解析被資源下載卡住。

defer#

defer 意旨為延遲(Deferred),在 HTML4.01 的規格中定義了:

When set, this boolean attribute provides a hint to the user agent that the script is not going to generate any document content (e.g., no “document.write” in javascript) and thus, the user agent can continue parsing and rendering.

也就是說,加上 defer 屬性後,瀏覽器會繼續解析、渲染畫面,而不會因為需要載入 <script> 內的資源而卡在那邊等;實際上的執行時間,會在 DOMContentLoaded 執行之前,由上到下的依照擺放順序觸發。

<script defer src="/path/to/script.js"></script>

W3C 規格定義 defer 為布林屬性,但 IE9 以前是自定義特規。即使寫成 <script defer="false"> 仍會有 defer 效果,使用時要特別注意。

async#

async 即為非同步(Asynchronous),在 HTML5 的規格中提到:

… If the async attribute is present, then the script will be executed asynchronously, as soon as it is available. …

<script> 加上 async 屬性後,與 defer 相同的是會在背景執行下載,但不同的是當下載完成會馬上暫停 DOM 解析(若尚未解析完的話),並開始執行 JavaScript。

也因為下載完成後會立即執行,加上 async 屬性後,就無法保證執行順序了。

這個屬性在標準中,同時也支援透過 JavaScript 動態塞入 <script> 的情境。例如:

const script = document.createElement("script");
script.src = "/something/awesome.js";
document.body.append(script);

動態建立的 <script>,預設就會是非同步載入;但可以透過屬性設定將它關閉:

script.async = false;

type=“module”#

在主流的現代瀏覽器中,<script> 的屬性可以加上 type="module"。這時,瀏覽器會將此檔案認為是一個 JavaScript 模組,其中的解析規則、執行環境會略有不同。

這時候的 <script> 預設行為會像是 defer 一樣,背景下載,且等待 DOM 解析、渲染完成後才執行,也因此 defer 屬性無法在 type="module" 產生作用。但同樣可以透過 async 屬性讓它在下載完成後即刻執行。

用法#

defer#

具備背景載入、不打斷渲染、保證執行順序的特性,沒特殊需求時建議全部設定。<script> 的擺放順序仍需留意。

async#

async 下載後會立刻執行且不保證順序,常見應用是完全獨立的小模組:

  • Google Analytics
  • 背景 Log
  • 頁面廣告

可在不影響使用者體驗的前提下,盡量提早開始產生效果。

現代前端開發大都透過 Webpack 等打包工具處理,鮮少需要自己設定這些屬性。開發者可透過 script-ext-html-webpack-plugin 等套件,為切分好的 Chunk 設定個別需要的 <script> 屬性。

延伸閱讀#

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

結語#

<script>asyncdefer 屬性設定大多已包在現代框架的打包流程中。唯有紮實認識這些網頁最基礎的規格,才能明白自己交出去的程式最後會產生什麼效果。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10216858