HTML 剖析器、指令碼擷取器與 JavaScript 引擎可以並行運作,但不同指令碼類型和屬性具有不同的執行限制。應根據相依性圖和就緒契約選擇模式,而不是僅將 async 或 defer 當作效能提示加入。
了解阻塞剖析器的傳統指令碼 #
未指定 async 或 defer 的外部傳統指令碼會在其元素所在位置阻塞剖析器。瀏覽器會先擷取並執行指令碼,再剖析後續標記:
<script src="/library.js"></script>
<script src="/application.js"></script>第二個指令碼可以讀取第一個指令碼建立的全域變數,因為文件順序就是執行順序。內嵌傳統指令碼也會在剖析器所在位置立即執行,因此只能存取截至該位置已剖析的 DOM。
當指令碼必須呼叫 document.write,或必須在解譯後續標記之前建立狀態時,阻塞剖析器可能是刻意的設計。應用程式程式碼應避免這類相依性,因為網路延遲會同時延後 DOM 建構和首次呈現。
延遲執行有序的傳統指令碼 #
defer 適用於外部傳統指令碼。指令碼擷取會與 HTML 剖析並行進行,接著延遲指令碼會在剖析完成後、DOMContentLoaded 之前,依文件順序執行:
<script defer src="/library.js"></script>
<script defer src="/application.js"></script>第二個指令碼仍然依賴第一個指令碼,但不會阻塞剖析器。defer 屬性對內嵌傳統指令碼沒有作用;請改用模組或明確的就緒回呼。
延遲指令碼仍可能因擷取緩慢或同步執行而延後 DOMContentLoaded。指令碼所需的樣式表也可能延後其執行,因為指令碼程式碼可能依賴計算後的樣式。
非同步載入獨立指令碼 #
具有 async 的外部傳統指令碼會在準備完成後立即執行。擷取不會阻塞剖析,但如果回應在剖析仍在進行時抵達,執行會暫停剖析器:
<script async src="https://analytics.example/sdk.js"></script>多個非同步指令碼之間不保證執行順序。它們不得相互依賴,不得依賴完整的 DOM,也不得假設會在 DOMContentLoaded 之前執行。DOMContentLoaded 不會等待這些指令碼,但視窗的 load 事件會等待屬於文件未完成載入工作一部分的指令碼。
如果後續程式碼依賴非同步初始化,請使用整合所提供的明確回呼或 promise。在某個網路環境中觀察到全域變數存在,並不能建立有效的順序契約。
將 ES 模組載入為相依性圖 #
type="module" 會同時改變載入和執行方式:
<script type="module" src="/main.js"></script>模組指令碼預設會延遲執行,內嵌模組也不例外。靜態 import 宣告會形成一張圖,其相依項會在模組評估前擷取。模組使用嚴格模式,具有自己的頂層範圍,並且每個解析後的模組識別只評估一次,而不是每個 <script> 元素評估一次。
模組擷取使用 CORS。跨來源模組及其所有跨來源相依項都必須回傳適當的 Access-Control-Allow-Origin 回應,且必須使用 JavaScript 相容的媒體類型。模組擷取失敗時,不會回退到傳統指令碼行為。
在模組指令碼加入 async 後,其圖可以在準備完成時立即評估,因而移除一般的文件順序行為:
<script type="module" async src="/independent-widget.js"></script>頂層 await 會暫停依賴等待中模組的其他模組評估。不要將 DOM 就緒視為應用程式初始化已完成的證明;當使用者必須等待非同步模組工作完成時,請匯出或公開初始化 promise。
動態插入指令碼 #
由 JavaScript 建立的指令碼元素預設具有強制非同步行為:
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 定義了可用的完成邊界:
await loadScript("/library.js");
await loadScript("/application.js");載入器可以在插入前設定 script.async = false,將動態插入的傳統指令碼放入有序佇列。當失敗需要停止相依載入時,明確的 promise 順序控制更清楚。動態 import() 是對應的模組機制,只有在要求的模組圖完成評估後才會解析。
在 DOMContentLoaded 之後動態加入指令碼,不會再次觸發該事件。在視窗 load 事件之前加入會延遲載入的工作,可能會延後 load;在事件發生後加入則無法重播該事件。
選擇正確的就緒事件 #
DOMContentLoaded 表示剖析已完成,且剖析器延遲指令碼已執行。這不表示影像、非同步指令碼、動態啟動的應用程式工作或所有自訂元素都已就緒:
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 上閱讀或評論(發表評論需要登入)