Script 标签中 defer 与 async 的区别
概述
在网页开发中,JavaScript 的加载和执行方式会直接影响页面的渲染性能。<script> 标签默认会阻塞 HTML 解析,如果脚本文件很大或网络很慢,用户就会看到白屏。HTML5 引入了 defer 和 async 两个属性,让开发者可以控制脚本的加载时机和执行时机,从而优化页面加载体验。
defer 和 async 的区别是前端面试的高频考点,也是实际开发中优化首屏加载速度的重要手段。理解它们的行为差异,能帮助你正确地组织页面中的脚本加载顺序。
默认行为:没有 defer 和 async
当 <script> 标签不添加任何属性时,浏览器遇到它会:
- 暂停 HTML 解析
- 下载脚本文件
- 执行脚本
- 执行完毕后,恢复 HTML 解析
<!DOCTYPE html>
<html>
<head>
<title>默认加载</title>
</head>
<body>
<h1>页面标题</h1>
<!-- 遇到这个标签,浏览器会停下来等脚本下载 + 执行完才继续 -->
<script src="app.js"></script>
<!-- 如果 app.js 很大,下面的内容会被延迟渲染 -->
<p>这段文字要等 app.js 执行完才能显示</p>
</body>
</html>
默认方式会造成渲染阻塞。如果把 <script> 放在 <head> 中,整个页面在脚本下载和执行完成之前都是白屏状态,用户体验很差。
defer:延迟执行
添加 defer 属性后,浏览器的行为变为:
- 不阻塞 HTML 解析,脚本在后台并行下载
- 等到 HTML 全部解析完成后,再按照脚本在文档中的顺序依次执行
- 在
DOMContentLoaded事件之前执行
<!DOCTYPE html>
<html>
<head>
<title>defer 加载</title>
<!-- defer 脚本:后台下载,HTML 解析完后按顺序执行 -->
<script defer src="vendor.js"></script>
<script defer src="app.js"></script>
</head>
<body>
<h1>页面标题</h1>
<p>这段文字不会被脚本加载阻塞,可以立即显示</p>
</body>
</html>
defer 的关键特性
| 特性 | 说明 |
|---|---|
| 是否阻塞解析 | 不阻塞,HTML 继续解析 |
| 下载时机 | 遇到标签时立即并行下载 |
| 执行时机 | HTML 解析完毕后、DOMContentLoaded 之前 |
| 执行顺序 | 保证顺序,按标签在文档中的出现顺序依次执行 |
| 适用范围 | 仅对外部脚本有效(必须有 src 属性) |
即使 app.js 比 vendor.js 先下载完,也会等 vendor.js 执行完后才执行 app.js。这对于有依赖关系的脚本非常重要。
async:异步执行
添加 async 属性后,浏览器的行为变为:
- 不阻塞 HTML 解析,脚本在后台并行下载
- 脚本下载完成后立即执行,执行时会暂停 HTML 解析
- 多个 async 脚本的执行顺序不确定,谁先下载完谁先执行
<!DOCTYPE html>
<html>
<head>
<title>async 加载</title>
<!-- async 脚本:后台下载,下载完立即执行 -->
<script async src="analytics.js"></script>
<script async src="ad.js"></script>
</head>
<body>
<h1>页面标题</h1>
<p>这段文字可能在脚本执行前显示,也可能在执行后显示</p>
</body>
</html>
async 的关键特性
| 特性 | 说明 |
|---|---|
| 是否阻塞解析 | 下载时不阻塞,执行时短暂阻塞 |
| 下载时机 | 遇到标签时立即并行下载 |
| 执行时机 | 下载完成后立即执行,不等 HTML 解析完 |
| 执行顺序 | 不保证顺序,谁先下载完谁先执行 |
| 适用范围 | 仅对外部脚本有效(必须有 src 属性) |
如果 ad.js(50KB)比 analytics.js(200KB)先下载完,那么 ad.js 会先执行。因此 async 脚本之间不能有依赖关系。
三种方式对比
执行时序对比图
核心区别对比表
| 对比维度 | 默认(无属性) | defer | async |
|---|---|---|---|
| HTML 解析 | 阻塞 | 不阻塞 | 下载不阻塞,执行短暂阻塞 |
| 下载方式 | 串行(阻塞后下载) | 并行(后台下载) | 并行(后台下载) |
| 执行时机 | 下载完立即执行 | HTML 解析完后执行 | 下载完立即执行 |
| 执行顺序 | 按文档顺序 | 按文档顺序 | 不保证顺序 |
| DOMContentLoaded | 等待脚本执行完 | 在事件之前执行 | 不确定(可能在事件前或后) |
| 能访问 DOM | 能(前面的 DOM) | 能(完整 DOM) | 不确定 |
| 适用场景 | 有 DOM 依赖的内联脚本 | 有依赖关系的外部脚本 | 独立的第三方脚本 |
实际使用场景
使用 defer 的场景
defer 适合需要操作 DOM 或有先后依赖关系的脚本:
<head>
<!-- 框架库先加载 -->
<script defer src="https://cdn.example.com/vue@3.js"></script>
<!-- 应用代码依赖 Vue,defer 保证顺序 -->
<script defer src="/js/app.js"></script>
</head>
典型场景:
- 主应用脚本(需要操作 DOM)
- 有依赖关系的多个脚本(如先加载框架,再加载插件)
- 需要在
DOMContentLoaded之前执行的脚本
使用 async 的场景
async 适合完全独立、不依赖 DOM 和其他脚本的第三方脚本:
<head>
<!-- 这些脚本互不依赖,加载完就可以执行 -->
<script async src="https://www.google-analytics.com/analytics.js"></script>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script async src="https://connect.facebook.net/sdk.js"></script>
</head>
典型场景:
- 数据统计和分析脚本(Google Analytics、百度统计等)
- 广告脚本
- 社交分享按钮脚本
- 其他不需要操作页面 DOM 的独立第三方脚本
传统方案:放在 body 底部
在 defer 和 async 出现之前,常见的优化方式是把 <script> 放在 </body> 之前:
<body>
<h1>页面内容</h1>
<p>所有内容先渲染完</p>
<!-- 放在底部,等 HTML 解析完再下载和执行 -->
<script src="app.js"></script>
</body>
两者看起来效果类似,但 defer 更优:
defer:HTML 解析的同时就开始下载脚本(并行),解析完后立即执行- 底部
<script>:等 HTML 解析到底部才开始下载脚本(串行),下载完再执行
defer 省去了等待下载的时间,首屏速度更快。
特殊情况与注意事项
1. 内联脚本不支持 defer 和 async
defer 和 async 仅对外部脚本(带 src 属性的)有效,对内联脚本无效:
<!-- ❌ defer 无效,会立即执行 -->
<script defer>
console.log('这段代码会立即执行');
</script>
<!-- ✅ defer 有效 -->
<script defer src="app.js"></script>
2. 动态创建的 script 默认是 async
通过 JavaScript 动态创建的 <script> 标签,默认行为等同于 async:
const script = document.createElement('script');
script.src = 'app.js';
// 动态脚本默认 async = true
console.log(script.async); // true
// 如果需要按顺序执行,手动设置 async = false
script.async = false;
document.head.appendChild(script);
3. module 类型的脚本默认是 defer
ES Module 脚本(type="module")默认带有 defer 行为:
<!-- type="module" 默认 defer 行为 -->
<script type="module" src="app.mjs"></script>
<!-- 等价于 -->
<script defer type="module" src="app.mjs"></script>
<!-- 如果想让 module 变成 async 行为 -->
<script async type="module" src="analytics.mjs"></script>
4. 同时写 defer 和 async
如果一个标签同时带有 defer 和 async,async 优先。但如果浏览器不支持 async,会回退到 defer 行为:
<!-- async 优先;不支持 async 的旧浏览器回退到 defer -->
<script defer async src="app.js"></script>
最佳实践
总结为几条简单规则:
- 应用主脚本 → 使用
defer,放在<head>中 - 有依赖关系的多个脚本 → 全部使用
defer,按依赖顺序排列 - 独立的第三方脚本(统计、广告等) → 使用
async - ES Module 脚本 → 默认就是
defer,不需要额外添加 - 避免把无属性的
<script>放在<head>中
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>最佳实践示例</title>
<!-- 1. 独立的第三方脚本:async -->
<script async src="https://www.google-analytics.com/analytics.js"></script>
<!-- 2. 应用脚本:defer,按依赖顺序排列 -->
<script defer src="/js/vendor.js"></script>
<script defer src="/js/utils.js"></script>
<script defer src="/js/app.js"></script>
</head>
<body>
<div id="app"></div>
<!-- 页面内容不会被脚本阻塞 -->
</body>
</html>
面试高频问答
Q1:script 标签的 defer 和 async 有什么区别?
A:两者都能让脚本异步下载,不阻塞 HTML 解析,区别在于执行时机和顺序:
defer:脚本在 HTML 解析完毕后、DOMContentLoaded事件触发前执行,多个 defer 脚本按文档顺序依次执行。async:脚本下载完成后立即执行,执行时会短暂阻塞 HTML 解析。多个 async 脚本的执行顺序不确定,谁先下载完谁先执行。
简单记忆:defer = 延迟有序,async = 异步无序。
Q2:什么时候用 defer,什么时候用 async?
A:
- 用
defer:脚本需要操作 DOM,或者多个脚本之间有依赖关系(如先加载 jQuery 再加载插件)。 - 用
async:脚本完全独立、不操作 DOM、不依赖其他脚本(如数据统计、广告、社交分享按钮等第三方脚本)。
大多数业务场景下推荐使用 defer,因为它既不阻塞解析又保证执行顺序。
Q3:defer 和把 script 放在 body 底部有什么区别?
A:两者都能让 HTML 先解析完再执行脚本,但关键区别在于下载时机:
defer:浏览器在解析 HTML 的同时就开始并行下载脚本,解析完后立即执行。- 底部
<script>:浏览器解析到</body>前的<script>标签时才开始下载,下载完后再执行。
defer 利用了解析 HTML 的空闲网络带宽来提前下载脚本,所以总体加载速度更快。
Q4:如果 defer 和 async 同时存在,浏览器会怎么处理?
A:如果同时存在,async 优先。浏览器会忽略 defer,按照 async 的方式处理脚本(下载完立即执行,不保证顺序)。同时写两个属性通常是为了兼容不支持 async 的旧浏览器,让它们回退到 defer 行为。
Q5:动态创建的 script 标签默认是什么行为?
A:通过 document.createElement('script') 创建的脚本标签,默认 async 为 true,也就是下载完立即执行,不保证顺序。如果需要按顺序执行,需要手动设置 script.async = false。
const script = document.createElement('script');
script.src = 'app.js';
script.async = false; // 设置为 false 才能保证顺序
document.head.appendChild(script);
Q6:type="module" 的 script 标签有什么特殊行为?
A:<script type="module"> 默认具有 defer 行为——异步下载,在 HTML 解析完后按顺序执行。如果需要像 async 那样下载完立即执行,可以显式添加 async 属性。此外,module 脚本还具有以下特性:
- 自动启用严格模式
- 拥有独立的模块作用域(顶层变量不会污染全局)
- 同一模块只会执行一次(即使被多次引入)
Q7:defer 脚本能保证在 DOMContentLoaded 之前执行吗?
A:是的,HTML 规范明确规定 defer 脚本会在 HTML 解析完成后、DOMContentLoaded 事件触发前按顺序执行。也就是说,在 DOMContentLoaded 的回调函数中,可以确保所有 defer 脚本已经执行完毕。
// 当 DOMContentLoaded 触发时,所有 defer 脚本已执行完
document.addEventListener('DOMContentLoaded', () => {
// 这里可以安全使用 defer 脚本中定义的变量和函数
});
Q8:async 脚本中可以安全操作 DOM 吗?
A:不安全。因为 async 脚本的执行时机不确定——可能在 HTML 解析到一半时就执行了。此时部分 DOM 元素可能还不存在,直接操作会报错。如果 async 脚本必须操作 DOM,应该在脚本内部监听 DOMContentLoaded 事件或使用 document.readyState 判断:
// async 脚本中安全操作 DOM 的方式
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
function init() {
// 在这里安全操作 DOM
}