JavaScript 指令碼載入

最後更新: 2025-09-19 20:13:39 +0800 +0800
原文以英文撰寫。您正在閱讀AI翻譯版本。

HTML 剖析器、指令碼擷取器與 JavaScript 引擎可以並行運作,但不同指令碼類型和屬性具有不同的執行限制。應根據相依性圖和就緒契約選擇模式,而不是僅將 async 或 defer 當作效能提示加入。

了解阻塞剖析器的傳統指令碼 #

未指定 async 或 defer 的外部傳統指令碼會在其元素所在位置阻塞剖析器。瀏覽器會先擷取並執行指令碼,再剖析後續標記:

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

第二個指令碼可以讀取第一個指令碼建立的全域變數,因為文件順序就是執行順序。內嵌傳統指令碼也會在剖析器所在位置立即執行,因此只能存取截至該位置已剖析的 DOM。

當指令碼必須呼叫 document.write,或必須在解譯後續標記之前建立狀態時,阻塞剖析器可能是刻意的設計。應用程式程式碼應避免這類相依性,因為網路延遲會同時延後 DOM 建構和首次呈現。

延遲執行有序的傳統指令碼 #

defer 適用於外部傳統指令碼。指令碼擷取會與 HTML 剖析並行進行,接著延遲指令碼會在剖析完成後、DOMContentLoaded 之前,依文件順序執行:

html
<script defer src="/library.js"></script>
<script defer src="/application.js"></script>

第二個指令碼仍然依賴第一個指令碼,但不會阻塞剖析器。defer 屬性對內嵌傳統指令碼沒有作用;請改用模組或明確的就緒回呼。

延遲指令碼仍可能因擷取緩慢或同步執行而延後 DOMContentLoaded。指令碼所需的樣式表也可能延後其執行,因為指令碼程式碼可能依賴計算後的樣式。

非同步載入獨立指令碼 #

具有 async 的外部傳統指令碼會在準備完成後立即執行。擷取不會阻塞剖析,但如果回應在剖析仍在進行時抵達,執行會暫停剖析器:

html
<script async src="https://analytics.example/sdk.js"></script>

多個非同步指令碼之間不保證執行順序。它們不得相互依賴,不得依賴完整的 DOM,也不得假設會在 DOMContentLoaded 之前執行。DOMContentLoaded 不會等待這些指令碼,但視窗的 load 事件會等待屬於文件未完成載入工作一部分的指令碼。

如果後續程式碼依賴非同步初始化,請使用整合所提供的明確回呼或 promise。在某個網路環境中觀察到全域變數存在,並不能建立有效的順序契約。

將 ES 模組載入為相依性圖 #

type="module" 會同時改變載入和執行方式:

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

模組指令碼預設會延遲執行,內嵌模組也不例外。靜態 import 宣告會形成一張圖,其相依項會在模組評估前擷取。模組使用嚴格模式,具有自己的頂層範圍,並且每個解析後的模組識別只評估一次,而不是每個 <script> 元素評估一次。

模組擷取使用 CORS。跨來源模組及其所有跨來源相依項都必須回傳適當的 Access-Control-Allow-Origin 回應,且必須使用 JavaScript 相容的媒體類型。模組擷取失敗時,不會回退到傳統指令碼行為。

在模組指令碼加入 async 後,其圖可以在準備完成時立即評估,因而移除一般的文件順序行為:

html
<script type="module" async src="/independent-widget.js"></script>

頂層 await 會暫停依賴等待中模組的其他模組評估。不要將 DOM 就緒視為應用程式初始化已完成的證明;當使用者必須等待非同步模組工作完成時,請匯出或公開初始化 promise。

動態插入指令碼 #

由 JavaScript 建立的指令碼元素預設具有強制非同步行為:

js
function loadScript(src) {
  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.src = src;
    script.addEventListener("load", resolve, { once: true });
    script.addEventListener("error", reject, { once: true });
    document.head.append(script);
  });
}

傳回的 promise 定義了可用的完成邊界:

js
await loadScript("/library.js");
await loadScript("/application.js");

載入器可以在插入前設定 script.async = false,將動態插入的傳統指令碼放入有序佇列。當失敗需要停止相依載入時,明確的 promise 順序控制更清楚。動態 import() 是對應的模組機制,只有在要求的模組圖完成評估後才會解析。

在 DOMContentLoaded 之後動態加入指令碼,不會再次觸發該事件。在視窗 load 事件之前加入會延遲載入的工作,可能會延後 load;在事件發生後加入則無法重播該事件。

選擇正確的就緒事件 #

DOMContentLoaded 表示剖析已完成,且剖析器延遲指令碼已執行。這不表示影像、非同步指令碼、動態啟動的應用程式工作或所有自訂元素都已就緒:

js
function start() {
  // The parsed DOM is available here.
}

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", start, { once: true });
} else {
  start();
}

對於可能在事件之後才開始的模組和動態載入程式碼,readyState 分支很重要。只有當版面配置或行為需要影像、框架等載入完成的資源時,才使用 load。當只涉及一個相依項時,請優先使用資源專用的 promise,例如 image.decode()、document.fonts.ready 或模組自身的初始化 promise。

阻塞剖析器的指令碼提供立即順序控制,defer 提供剖析後的有序執行,async 提供獨立的機會式執行,而模組提供經過 CORS 檢查的相依性圖。讓機制符合相依性契約,可以消除在不同快取或網路條件下會失效的時序假設。

評論

在 GitHub 上閱讀或評論(發表評論需要登入)