🧭 引言:为什么脚本加载如此重要?
在现代 Web 应用中,JavaScript 的加载方式直接影响页面的首屏渲染时间、交互响应速度和用户体验。据统计,脚本阻塞导致的渲染延迟是页面性能瓶颈的主要来源之一。
为此,HTML5 引入了 defer 与 async 两个属性,用于优化脚本的加载与执行时机。但许多开发者对它们的区别和使用场景仍感到困惑。本文将带你由浅入深,彻底掌握 defer 与 async 的原理、差异与实战技巧。
🧱 一、基础知识:浏览器如何加载脚本?
在默认情况下,浏览器遇到 <script> 标签时会:
- 暂停 HTML 解析
- 立即下载并执行脚本
- 继续解析 HTML
这称为**“渲染阻塞”**,会导致页面白屏时间变长,尤其在脚本体积较大或网络较慢时。
✅ 解决方案:异步加载脚本,即使用
defer或async。
⚙️ 二、defer 与 async 的定义与行为对比
| 特性 | defer | async |
|---|---|---|
| 是否阻塞 HTML 解析 | ❌ 不阻塞 | ❌ 不阻塞 |
| 脚本执行时机 | 文档解析完成后,DOMContentLoaded 前 | 脚本下载完成后立即执行 |
| 执行顺序 | 保持声明顺序 | 不保证顺序(谁先下载完谁先执行) |
| 适用场景 | 依赖 DOM 或其他脚本 | 独立脚本,如统计、广告 |
| 是否仅对外部脚本有效 | ✅ 是 | ✅ 是 |
📌 总结一句话:
defer 是“渲染完再执行”,async 是“下载完就执行”()
🧪 三、示例代码演示
✅ 使用 defer:按顺序延迟执行
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Defer 示例</title>
<script defer src="jquery.js"></script>
<script defer src="main.js"></script>
</head>
<body>
<h1>Hello, defer!</h1>
</body>
</html>
jquery.js和main.js会并行下载- 但执行顺序固定:先 jQuery,再 main
- 等 HTML 解析完成后才执行,可安全操作 DOM
✅ 使用 async:快速独立执行
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Async 示例</title>
<script async src="analytics.js"></script>
<script async src="ad.js"></script>
</head>
<body>
<h1>Hello, async!</h1>
</body>
</html>
analytics.js和ad.js谁先下载完谁先执行- 不保证顺序,也不等待 HTML 解析完成
- 适合不依赖 DOM 或其他脚本的独立模块
🧠 四、深入原理:浏览器内部发生了什么?
🔄 事件时间线对比(简化版)
| 阶段 | 默认脚本 | defer 脚本 | async 脚本 |
|---|---|---|---|
| HTML 解析 | 阻塞 | ✅ 继续解析 | ✅ 继续解析 |
| 脚本下载 | 同步 | 异步并行 | 异步并行 |
| 脚本执行 | 立即 | 解析完成后按序 | 下载完立即执行 |
触发 DOMContentLoaded | 等待脚本 | 所有 defer 执行后 | 不等待 async |
🎯 五、应用场景与最佳实践
✅ 什么时候用 defer?
- 脚本需要操作 DOM
- 脚本依赖其他脚本(如 jQuery → 插件)
- 希望按顺序执行
- 示例:主应用逻辑、UI 初始化、框架入口
<script defer src="framework.js"></script>
<script defer src="app.js"></script>
✅ 什么时候用 async?
- 脚本是独立的,不依赖其他代码
- 希望尽早执行,不在意顺序
- 示例:第三方统计、广告、A/B 测试、社交分享按钮
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
❌ 不推荐的使用方式
| 错误做法 | 原因 |
|---|---|
给内联脚本加 defer 或 async | 会被浏览器忽略() |
对相互依赖的脚本使用 async | 可能导致运行时错误(顺序不确定) |
在模块脚本(type="module")上使用 defer | 模块默认就是 defer,无需重复添加() |
📊 六、性能对比:真实数据说话
根据 Web.dev 和 [Chrome DevTools] 的测试:
| 加载方式 | 首屏时间 | 脚本执行顺序 | 白屏时间 |
|---|---|---|---|
| 默认阻塞 | 慢 | 保证 | 长 |
defer | ✅ 快 | ✅ 保证 | ✅ 短 |
async | ✅ 快 | ❌ 不保证 | ✅ 短(但可能闪烁) |
💡 结论:优先使用
defer,除非脚本完全独立。
🧩 七、进阶技巧:组合使用与动态加载
1. 混合使用 defer 与 async
你可以在同一页面中组合使用二者,只要清楚它们的执行顺序:
<script defer src="lib.js"></script> <!-- 先执行 -->
<script async src="tracker.js"></script> <!-- 可能早执行,但不依赖 lib -->
<script defer src="app.js"></script> <!-- 等 lib 执行后执行 -->
2. 动态加载脚本(编程式)
function loadScript(src, async = true) {
const script = document.createElement('script');
script.src = src;
script.async = async;
document.head.appendChild(script);
}
loadScript('https://example.com/widget.js', true); // async
loadScript('/js/main.js', false); // defer-like behavior if inserted early
📚 八、总结:一张图记住所有区别
🖼️ 推荐保存下图,面试/开发必备!
[HTML 解析] ───────┬──────────────────────────────┐
│ │
┌─────────▼──────────┐ ┌───────────▼──────────┐
│ <script defer> │ │ <script async> │
│ 下载:异步 │ │ 下载:异步 │
│ 执行:解析后按序 │ │ 执行:下载完立即 │
│ 顺序:保证 │ │ 顺序:不保证 │
└─────────────────────┘ └──────────────────────┘
🧭 九、开发建议与 checklist
✅ 最佳实践速查表:
| 任务 | 推荐属性 |
|---|---|
| 主框架(React、Vue) | defer |
| jQuery + 插件 | defer |
| 网站统计(Google Analytics) | async |
| 广告代码 | async |
| 不依赖 DOM 的 SDK | async |
| 依赖 DOM 的初始化脚本 | defer |
🔒 安全提示:
使用async加载第三方脚本时,建议加integrity属性防止篡改:
<script async src="https://example.com/sdk.js"
integrity="sha384-oqVu..."></script>
📖 十、延伸阅读与工具
- MDN
<script>元素文档() - Web.dev: Efficiently load JavaScript
- Lighthouse:检测阻塞脚本
- [Chrome DevTools Coverage]:分析脚本是否被使用
📣 结语:选择比努力更重要
🎯 defer 是默认的最佳选择,async 是独立脚本的加速器。
理解 defer 与 async 的差异,不仅能提升页面性能,更能避免潜在的执行顺序 Bug。希望本文能帮助你在实际项目中做出更明智的脚本加载决策!
💬 有任何疑问或补充,欢迎在评论区留言交流!