魯斯前端布魯斯前端

文章中英模式

常見的前端面試題目 - 頁面載入 - Script 位置與 defer、async 屬性

深入解析 JavaScript Script 標籤的最佳放置位置,以及 defer 與 async 屬性的使用時機與效果,幫助你提升網頁效能與用戶體驗。

影片縮圖

懶得看文章?那就來看影片吧

Script 標籤的位置

網頁中 JavaScript 的載入與執行時機直接影響使用者體驗。根據擺放位置的不同,script 標籤的行為也有所差異:

  1. 1.
    放在 <head>:瀏覽器會在解析 HTML 前先下載並執行 JavaScript,這會阻塞頁面渲染,導致頁面載入速度變慢。
  2. 2.
    放在 </body>:瀏覽器會先渲染完 HTML 內容,再下載並執行 JavaScript,用戶可以更快看到頁面內容,但可能在 JavaScript 載入前無法使用某些功能。

defer 屬性的用途

defer 屬性讓腳本的下載與 HTML 解析同時進行,但會延遲執行直到 HTML 解析完成:

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

defer 的特點:

  • 不會阻塞 HTML 解析與渲染
  • 保證按照在 HTML 中的順序執行
  • 在 DOMContentLoaded 事件之前執行
  • 僅對外部腳本(有 src 屬性)有效

適用場景:

  • 需要 DOM 完整載入後才能執行的腳本
  • 依賴於其他腳本順序的執行
  • 不急需立即執行的腳本

module 腳本的用途

type="module" 讓瀏覽器將腳本視為 JavaScript 模組,支持 import/export 語法:

<script type="module" src="app.js"></script>

module 與 defer 的區別:

  • module 腳本預設具有 defer 行為,無需額外添加 defer 屬性
  • module 支持 import/export 語法,而 defer 腳本不支持
  • module 腳本自動採用嚴格模式 (strict mode)

必須使用 module 而非 defer 的場景:

  • 需要使用 import/export 語法時
  • 需要模組化程式碼以提高可維護性
// math.js (模組)
export function add(a, b) {
  return a + b;
}

// app.js (使用模組)
import { add } from './math.js';

document.addEventListener('DOMContentLoaded', () => {
  console.log(add(2, 3)); // 輸出: 5
});

async 屬性的用途

async 屬性讓腳本的下載與 HTML 解析同時進行,下載完成後立即執行:

<script async src="script.js"></script>

async 的特點:

  • 不會阻塞 HTML 解析與渲染
  • 不保證執行順序,先下載完的先執行
  • 執行時會暫停 HTML 解析
  • 僅對外部腳本有效
  • 不一定在 DOMContentLoaded 事件前執行

適用場景:

  • 獨立的、不依賴其他腳本的功能
  • 分析腳本、廣告腳本等第三方腳本
  • 不需要操作 DOM 的腳本

圖解比較

以下圖解展示了不同腳本載入方式的行為差異:

──────────────────────────── 時間軸 ────────────────────────────>

【無屬性 - 放在 </body> 前】
HTML 解析:  ■■■■■■■■■■■■■■■■■■■■■■■■■■HTML解析■■■■■■■■■■■■■■■■■■
            ↑                                      ↑
            開始解析                               解析完成
            
腳本處理:                                         ■■■下載+執行■■■
                                                  ↑            ↑
                                                  開始下載      執行完成
<body>
  <!-- 內容 -->
  <script src="script.js"></script>  <!-- 傳統做法 -->
</body>

--------------------------------

【defer】
HTML 解析:  ■■■■■■■■■■■■■■■■■■■■■■■■■■HTML解析■■■■■■■■■■■■■■■■■■→DOM就緒
            ↑                                      ↑            ↑
            開始解析                               解析完成      DOMContentLoaded
            
腳本處理:   ■■■■■■■■■■■■■下載■■■■■■■■■■■■■             ■■■執行■■■
            ↑                              ↑                   ↑      ↑
            開始下載                       下載完成             開始執行  執行完成
            
<head>
  <script defer src="script.js"></script>  <!-- 現代推薦做法 -->
</head>

--------------------------------

【async】
HTML 解析:  ■■■■■■■■■■■■■■■■■暫停■■■■■■■■■剩餘HTML解析■■■■■■■■■■■
            ↑                    ↑         ↑                  ↑
            開始解析             暫停      繼續解析            解析完成
            
腳本處理:   ■■■■■■■下載■■■■■■■■■執行■■■
            ↑                  ↑    ↑
            開始下載           下載完成 執行完成
            
<head>
  <script async src="script.js"></script>  <!-- 適用於獨立腳本 -->
</head>

最佳實踐

  1. 1.
    現代推薦做法:將帶有 defer 屬性的腳本放在 <head>
    <head>
      <script defer src="main.js"></script>
    </head>

    這樣可以盡早開始下載腳本,同時不阻塞 HTML 解析,提升頁面載入體驗。

  2. 2.
    獨立腳本:使用 async 屬性
    <head>
      <script async src="analytics.js"></script>
    </head>
  3. 3.
    傳統做法:將腳本放在 </body>
    <body>
      <!-- 頁面內容 -->
      <script src="main.js"></script>
    </body>
  4. 4.
    核心功能且時間敏感:不使用屬性,直接放在 <head>
    <head>
      <script src="critical.js"></script>
    </head>

🔥 常見面試題目

(一)defer 和 async 有什麼區別?

解答:兩者主要區別在於執行時機和順序:

  • defer:在 HTML 解析完成後按照在 HTML 中的順序執行
  • async:下載完成後立即執行,不保證順序,會中斷 HTML 解析
<!-- defer 範例:按順序執行 -->
<head>
  <script defer src="first.js"></script>
  <script defer src="second.js"></script>
  <!-- first.js 一定會在 second.js 之前執行 -->
</head>

<!-- async 範例:不保證順序 -->
<head>
  <script async src="analytics.js"></script>
  <script async src="ads.js"></script>
  <!-- 哪個先下載完成就先執行哪個 -->
</head>

(二)為什麼不建議將所有腳本都放在 <head> 中且不使用 defer 或 async?

解答:這會造成瀏覽器必須先下載並執行所有 JavaScript 才開始渲染頁面,導致用戶看到白屏時間延長,嚴重影響首次內容繪製(FCP)和用戶體驗。

<!-- 不推薦:會阻塞渲染 -->
<head>
  <script src="large-library.js"></script>
  <script src="app.js"></script>
  <script src="components.js"></script>
  <!-- 頁面會等待所有腳本下載和執行完才開始渲染 -->
</head>

<!-- 推薦:使用 defer -->
<head>
  <script defer src="large-library.js"></script>
  <script defer src="app.js"></script>
  <script defer src="components.js"></script>
  <!-- HTML 解析不會被阻塞,腳本會在 DOMContentLoaded 前執行 -->
</head>

(三)何時選擇 defer,何時選擇 async?

解答:

  • 選擇 defer:當腳本需要操作 DOM 或依賴其他腳本的執行順序
  • 選擇 async:當腳本完全獨立,不需要保證順序,如分析工具、廣告等
<!-- 使用 defer 的情境:需要 DOM 和執行順序 -->
<head>
  <script defer src="jquery.js"></script>
  <script defer src="jquery-plugin.js"></script>
  <script defer src="app.js">
    // 這個腳本依賴 jquery 和 jquery-plugin
    // 且可能需要操作 DOM 元素
  </script>
</head>

<!-- 使用 async 的情境:獨立功能 -->
<head>
  <script async src="google-analytics.js"></script>
  <script async src="facebook-pixel.js"></script>
  <!-- 這些腳本彼此獨立,不需要特定順序,也不依賴 DOM -->
</head>

(四)module 腳本(type="module")的載入行為如何?

解答:module 腳本預設就具有 defer 的行為,即使沒有顯式設定 defer 屬性。如果想要 async 行為,需要明確添加 async 屬性。

<!-- 預設具有 defer 行為 -->
<head>
  <script type="module" src="app.js"></script>
  <!-- 等同於 <script type="module" defer src="app.js"></script> -->
  
  <!-- 如果需要 async 行為,必須明確指定 -->
  <script type="module" async src="independent-module.js"></script>
</head>

(五)inline script 可以使用 defer 或 async 嗎?

解答:不可以。defer 和 async 屬性僅對外部腳本(有 src 屬性的 script 標籤)有效,對內聯腳本沒有效果。

<!-- 無效:defer 和 async 對內聯腳本無效 -->
<head>
  <script defer>
    console.log("這個 defer 屬性不起作用");
    // 這個腳本會立即執行,阻塞 HTML 解析
  </script>
  
  <script async>
    console.log("這個 async 屬性不起作用");
    // 這個腳本也會立即執行
  </script>
  
  <!-- 有效:外部腳本可以使用 defer 或 async -->
  <script defer src="external.js"></script>
</head>