Skip to content

延迟与异步加载(defer / async / module)

JavaScript 的加载方式直接影响页面的渲染速度。默认情况下,<script> 标签会阻塞 HTML 的解析,直到脚本下载和执行完毕。deferasynctype="module" 三种属性提供了不同的异步加载策略。本节将详细对比三种加载方式的时序差异、渲染阻塞行为,以及各自的适用场景。

前置知识

阅读本节前,建议先了解:资源加载优先级

script 默认行为

html
<!-- 默认行为:阻塞 HTML 解析 -->
<script src="app.js"></script>

<!-- 执行顺序:
  1. 浏览器开始解析 HTML
  2. 遇到 script 标签
  3. 暂停 HTML 解析
  4. 下载 app.js
  5. 执行 app.js
  6. 恢复 HTML 解析
-->

三种加载方式对比

时序对比

HTML 解析:  |====|-----|==========|
script:      |下载|执行|
HTML 解析:  |====|下载|执行|=====|

defer:
HTML 解析:  |=======================|
defer script:|-------下载-------|执行|
                                   ↑ HTML解析完成后按顺序执行

async:
HTML 解析:  |========|=====|===|
async script: |---下载--|执行|
                ↑ 下载完成后立即执行,可能在HTML解析中

module:
HTML 解析:  |=======================|
module script:|-------下载-------|执行|
                                   ↑ 默认类似 defer,但总是 defer

对比表

特性无属性deferasyncmodule
下载阻塞解析并行下载并行下载并行下载
执行时机下载后立即HTML解析后下载完成后HTML解析后
顺序保证是(按DOM)是(按DOM)否(先完成先执行)是(按DOM)
阻塞解析
DOM ready之前之前不确定之前
内联支持否(直接执行)不适用不适用
适用场景依赖 DOM大多数脚本独立第三方脚本现代项目

defer 详解

基本用法

html
<!-- defer:延迟执行 -->
<script src="app.js" defer></script>
<script src="utils.js" defer></script>
<script src="analytics.js" defer></script>

特性

  1. 下载不阻塞:脚本在后台下载,HTML 解析继续
  2. 按顺序执行:多个 defer 脚本按 DOM 顺序执行
  3. DOM ready 前执行:在 DOMContentLoaded 之前
  4. 可以访问 DOM:执行时 DOM 已构建完成

适用场景

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>defer 示例</title>
</head>
<body>
  <!-- 页面内容先渲染 -->

  <script defer src="vendor.js"></script>
  <script defer src="app.js"></script>
  <!-- vendor.js 在 app.js 之前执行 -->
</body>
</html>

async 详解

基本用法

html
<!-- async:异步执行 -->
<script src="analytics.js" async></script>
<script src="ads.js" async></script>

特性

  1. 下载不阻塞:脚本在后台下载
  2. 不保证顺序:先下载完成的先执行
  3. 下载完立即执行:可能在 HTML 解析过程中
  4. 不保证 DOM ready:执行时 DOM 可能未完成

适用场景

html
<!-- 独立的第三方脚本 -->
<script async src="https://www.google-analytics.com/analytics.js"></script>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>

<!-- 不依赖 DOM 的独立脚本 -->
<script async src="/scripts/worker.js"></script>

type="module" 详解

基本用法

html
<!-- ES Module 脚本默认 defer 行为 -->
<script type="module" src="app.mjs"></script>

特性

  1. 默认 defer:总是延迟执行
  2. 严格模式:自动启用 'use strict'
  3. 作用域隔离:模块有自己的作用域
  4. 可导入:支持 import/export
html
<script type="module">
  import { initApp } from './modules/app.mjs';

  // DOM 已构建完成,可以安全操作 DOM
  initApp();
</script>

nomodule 降级

html
<!-- 现代浏览器加载 module -->
<script type="module" src="app.mjs"></script>

<!-- 旧浏览器加载普通脚本 -->
<script nomodule src="app.bundle.js"></script>

最佳实践

推荐的脚本加载策略

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>脚本加载优化示例</title>

  <!-- 关键 CSS 内联 -->
  <style>
    body { margin: 0; font-family: sans-serif; }
  </style>

  <!-- 非关键 CSS 异步加载 -->
  <link rel="preload" href="styles/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
</head>
<body>
  <!-- 首屏内容 -->

  <!-- 推荐策略:所有脚本使用 defer -->
  <script defer src="vendor.js"></script>
  <script defer src="app.js"></script>

  <!-- 第三方独立脚本使用 async -->
  <script async src="https://www.google-analytics.com/analytics.js"></script>

  <!-- 或使用 module -->
  <script type="module" src="app.mjs"></script>
  <script nomodule src="app.bundle.js"></script>
</body>
</html>

注意事项

  1. defer 脚本按顺序执行:适合有依赖关系的脚本
  2. async 脚本不保证顺序:适合独立的第三方脚本
  3. module 默认是 defer:不需要额外添加 defer
  4. 内联 script 不支持 defer/async:内联脚本总是同步执行
  5. 不要混合使用 defer 和 async:在同一个页面中保持一致

最佳实践

  • 大多数脚本使用 defer
  • 独立第三方脚本使用 async
  • 现代项目使用 type="module"
  • 脚本放在 <body> 底部
  • 关键首屏脚本可以内联
  • 使用 nomodule 为旧浏览器提供降级方案

下一节

继续学习:图片优化策略

参考链接