Skip to main content

script tag 加上 async & defer 的功能及差異?

1. <script> 標籤的基本行為

在 HTML 中,<script> 標籤用來載入 JavaScript 檔案或內嵌 JavaScript 程式碼。預設情況下,當瀏覽器解析 HTML 時,遇到 <script> 標籤會:

  1. 停止解析 HTML:瀏覽器會暫停 HTML 的解析,優先下載並執行 JavaScript 檔案。

  2. 同步執行:JavaScript 檔案會按照 <script> 標籤出現在 HTML 文件中的順序,依序下載並執行。

  3. 阻塞渲染:在 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。