script tag 加上 async & defer 的功能及差異?
1. <script>
標籤的基本行為
在 HTML 中,<script>
標籤用來載入 JavaScript 檔案或內嵌 JavaScript 程式碼。預設情況下,當瀏覽器解析 HTML 時,遇到 <script>
標籤會:
-
停止解析 HTML:瀏覽器會暫停 HTML 的解析,優先下載並執行 JavaScript 檔案。
-
同步執行:JavaScript 檔案會按照
<script>
標籤出現在 HTML 文件中的順序,依序下載並執行。 -
阻塞渲染:在 JavaScript 下載和執行完成前,頁面的 DOM 結構不會繼續解析,這可能導致頁面載入速度變慢。
這種行為對於依賴 DOM 的腳本可能會造成問題,特別是當 JavaScript 檔案較大或網路速度較慢時。因此,async 和 defer 屬性被引入來優化腳本載入的方式。
2. async 屬性的功能
-
功能:當
<script>
標籤加上 async 屬性時,JavaScript 檔案會非同步載入,也就是說:-
瀏覽器會在解析 HTML 的同時,平行下載 JavaScript 檔案。
-
一旦 JavaScript 檔案下載完成,瀏覽器會立即執行該腳本,不等待 HTML 解析完成。
-
執行順序不保證,取決於哪個腳本先下載完成。
-
-
適用場景:
-
腳本不依賴 DOM 或其他腳本(例如獨立的分析工具、廣告腳本)。
-
希望腳本盡快執行,但不關心執行順序。
-
-
注意事項:
-
如果腳本依賴 DOM,可能會因為 DOM 尚未解析完成而導致錯誤(例如找不到某個元素)。
-
多個 async 腳本之間的執行順序不可預測。
-
範例程式碼(使用 async)
假設你有一個 HTML 文件,載入一個獨立的計數器腳本 counter.js,不需要依賴 DOM:
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<title>Async 範例</title>
</head>
<body>
<h1>這是一個測試頁面</h1>
<div id="counter">計數器:0</div>
<!-- 使用 async 載入 counter.js -->
<script async src="counter.js"></script>
</body>
</html>
counter.js 的內容如下:
// counter.js
console.log("計數器腳本已載入");
let count = 0;
setInterval(() => {
count++;
console.log(`計數:${count}`);
}, 1000);
說明:
-
瀏覽器會在解析 HTML 的同時開始下載 counter.js。
-
一旦 counter.js 下載完成,會立即執行,開始每秒打印計數。
-
但如果 counter.js 需要操作 DOM(例如 document.getElementById('counter')),可能會因為 DOM 尚未完全解析而失敗。
3. defer 屬性的功能
-
功能:當
<script>
標籤加上 defer 屬性時,JavaScript 檔案同樣會非同步載入,但行為不同:-
瀏覽器會在解析 HTML 的同時,平行下載 JavaScript 檔案。
-
JavaScript 檔案會等到 HTML 完全解析完成(DOM 構建完畢)後才執行。
-
執行順序會按照
<script>
標籤在 HTML 中的順序執行。
-
-
適用場景:
-
腳本需要操作 DOM(例如修改頁面元素)。
-
多個腳本之間有依賴關係,需要保證執行順序。
-
-
注意事項:
-
defer 不會阻塞 HTML 解析,適合大多數需要操作 DOM 的腳本。
-
比 async 更常用,因為它保證執行順序且等待 DOM 就緒。
-
範例程式碼(使用 defer)
假設你有一個 HTML 文件,載入一個腳本 updateDOM.js 用來更新頁面上的計數器:
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<title>Defer 範例</title>
</head>
<body>
<h1>這是一個測試頁面</h1>
<div id="counter">計數器:0</div>
<!-- 使用 defer 載入 updateDOM.js -->
<script defer src="updateDOM.js"></script>
</body>
</html>
updateDOM.js 的內容如下:
// updateDOM.js
console.log("DOM 更新腳本已載入");
const counterElement = document.getElementById('counter');
let count = 0;
setInterval(() => {
count++;
counterElement.textContent = `計數器:${count}`;
}, 1000);
說明:
-
瀏覽器會在解析 HTML 的同時開始下載 updateDOM.js。
-
updateDOM.js 會等到 HTML 解析完成(DOM 構建完畢)後才執行。
-
因為 DOM 已就緒,
document.getElementById('counter')
能正確找到元素並更新內容。
4. async 與 defer 的差異
以下是 async 和 defer 的主要差異,整理成表格方便理解:
屬性 | 下載方式 | 執行時機 | 執行順序 | 適用場景 |
---|---|---|---|---|
無屬性 | 同步下載 | 下載完成後立即執行,阻塞 HTML 解析 | 按 <script> 順序 | 腳本必須立即執行且不依賴其他腳本 |
async | 非同步下載 | 下載完成後立即執行,不等待 HTML 解析 | 不保證順序 | 獨立腳本(例如分析工具) |
defer | 非同步下載 | HTML 解析完成後執行 | 按 <script> 順序 | 依賴 DOM 或其他腳本的腳本 |
圖解說明:
-
無屬性:HTML 解析 → 暫停解析 → 下載腳本 → 執行腳本 → 繼續解析 HTML。
-
async:HTML 解析與腳本下載平行進行 → 腳本下載完成後立即執行(可能中斷 HTML 解析)。
-
defer:HTML 解析與腳本下載平行進行 → 等待 HTML 解析完成後按順序執行腳本。
5. 結合 async 和 defer
-
如果同時在
<script>
標籤上使用 async 和 defer,瀏覽器的行為會因瀏覽器版本而異:-
現代瀏覽器:通常優先使用 async,忽略 defer。
-
舊版瀏覽器:可能會根據具體實現有所不同。
-
-
建議:不要同時使用這兩個屬性,選擇最適合你需求的屬性即可。
範例程式碼(多個腳本依賴)
假設你有兩個腳本:utils.js(工具函數)和 main.js(依賴 utils.js 的邏輯),使用 defer 確保順序:
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<title>多腳本範例</title>
</head>
<body>
<h1>這是一個測試頁面</h1>
<div id="counter">計數器:0</div>
<!-- 使用 defer 確保 utils.js 先於 main.js 執行 -->
<script defer src="utils.js"></script>
<script defer src="main.js"></script>
</body>
</html>
utils.js 的內容:
// utils.js
window.updateCounter = function(element, count) {
element.textContent = `計數器:${count}`;
};
console.log("utils.js 已載入");
main.js 的內容:
// main.js
console.log("main.js 已載入");
const counterElement = document.getElementById('counter');
let count = 0;
setInterval(() => {
count++;
window.updateCounter(counterElement, count); // 調用 utils.js 的函數
}, 1000);
說明:
-
兩個腳本都使用 defer,因此它們會在 HTML 解析完成後按順序執行(utils.js 先執行,main.js 後執行)。
-
因為 utils.js 先執行,window.updateCounter 函數會在 main.js 使用前已經定義好。
6. 實際應用建議
-
使用 defer:
-
大多數情況下,defer 是更好的選擇,因為它保證腳本在 DOM 就緒後執行,且保持執行順序。
-
適用於需要操作 DOM 或有依賴關係的腳本。
-
-
使用 async:
-
當腳本是獨立的、不依賴 DOM 或其他腳本時(例如 Google Analytics)。
-
需要腳本盡快執行,但不關心順序。
-
-
不使用屬性:
-
當腳本必須立即執行且不依賴其他腳本(例如初始化某些關鍵功能)。
-
但這可能會阻塞頁面渲染,應謹慎使用。
-
7. 常見問題與解決方法
-
問題:腳本執行時找不到 DOM 元素?
-
解決:使用 defer 或在腳本中使用 DOMContentLoaded 事件監聽:
document.addEventListener('DOMContentLoaded', () => {
const counterElement = document.getElementById('counter');
// 你的程式碼
});
-
-
問題:多個腳本執行順序錯亂?
- 解決:使用 defer 確保腳本按順序執行,或明確指定依賴關係。
-
問題:腳本檔案很大,載入時間長?
- 解決:使用 defer 或 async 避免阻塞 HTML 解析,並考慮壓縮腳本檔案或使用 CDN。