鲁斯前端布鲁斯前端

文章中英模式

常见的前端面试题目 - 页面加载 - 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> -->
  
  <!-- If async behavior is needed, must be explicitly specified -->
  <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>