延迟与异步加载(defer / async / module)
JavaScript 的加载方式直接影响页面的渲染速度。默认情况下,<script> 标签会阻塞 HTML 的解析,直到脚本下载和执行完毕。defer、async 和 type="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对比表
| 特性 | 无属性 | defer | async | module |
|---|---|---|---|---|
| 下载 | 阻塞解析 | 并行下载 | 并行下载 | 并行下载 |
| 执行时机 | 下载后立即 | 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>特性
- 下载不阻塞:脚本在后台下载,HTML 解析继续
- 按顺序执行:多个 defer 脚本按 DOM 顺序执行
- DOM ready 前执行:在
DOMContentLoaded之前 - 可以访问 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>特性
- 下载不阻塞:脚本在后台下载
- 不保证顺序:先下载完成的先执行
- 下载完立即执行:可能在 HTML 解析过程中
- 不保证 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>特性
- 默认 defer:总是延迟执行
- 严格模式:自动启用
'use strict' - 作用域隔离:模块有自己的作用域
- 可导入:支持
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>注意事项
- defer 脚本按顺序执行:适合有依赖关系的脚本
- async 脚本不保证顺序:适合独立的第三方脚本
- module 默认是 defer:不需要额外添加 defer
- 内联 script 不支持 defer/async:内联脚本总是同步执行
- 不要混合使用 defer 和 async:在同一个页面中保持一致
最佳实践
- 大多数脚本使用
defer - 独立第三方脚本使用
async - 现代项目使用
type="module" - 脚本放在
<body>底部 - 关键首屏脚本可以内联
- 使用 nomodule 为旧浏览器提供降级方案
下一节
继续学习:图片优化策略