跳到主要内容

Script 标签中 defer 与 async 的区别

概述

在网页开发中,JavaScript 的加载和执行方式会直接影响页面的渲染性能<script> 标签默认会阻塞 HTML 解析,如果脚本文件很大或网络很慢,用户就会看到白屏。HTML5 引入了 deferasync 两个属性,让开发者可以控制脚本的加载时机执行时机,从而优化页面加载体验。

为什么要学这个?

deferasync 的区别是前端面试的高频考点,也是实际开发中优化首屏加载速度的重要手段。理解它们的行为差异,能帮助你正确地组织页面中的脚本加载顺序。


默认行为:没有 defer 和 async

<script> 标签不添加任何属性时,浏览器遇到它会:

  1. 暂停 HTML 解析
  2. 下载脚本文件
  3. 执行脚本
  4. 执行完毕后,恢复 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 属性后,浏览器的行为变为:

  1. 不阻塞 HTML 解析,脚本在后台并行下载
  2. 等到 HTML 全部解析完成后,再按照脚本在文档中的顺序依次执行
  3. 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.jsvendor.js 先下载完,也会等 vendor.js 执行完后才执行 app.js。这对于有依赖关系的脚本非常重要。


async:异步执行

添加 async 属性后,浏览器的行为变为:

  1. 不阻塞 HTML 解析,脚本在后台并行下载
  2. 脚本下载完成后立即执行,执行时会暂停 HTML 解析
  3. 多个 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 脚本之间不能有依赖关系


三种方式对比

执行时序对比图

核心区别对比表

对比维度默认(无属性)deferasync
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 底部

deferasync 出现之前,常见的优化方式是把 <script> 放在 </body> 之前:

<body>
<h1>页面内容</h1>
<p>所有内容先渲染完</p>

<!-- 放在底部,等 HTML 解析完再下载和执行 -->
<script src="app.js"></script>
</body>
defer vs 放在 body 底部

两者看起来效果类似,但 defer 更优:

  • defer:HTML 解析的同时就开始下载脚本(并行),解析完后立即执行
  • 底部 <script>:等 HTML 解析到底部才开始下载脚本(串行),下载完再执行

defer 省去了等待下载的时间,首屏速度更快


特殊情况与注意事项

1. 内联脚本不支持 defer 和 async

deferasync 仅对外部脚本(带 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

如果一个标签同时带有 deferasyncasync 优先。但如果浏览器不支持 async,会回退到 defer 行为:

<!-- async 优先;不支持 async 的旧浏览器回退到 defer -->
<script defer async src="app.js"></script>

最佳实践

总结为几条简单规则:

  1. 应用主脚本 → 使用 defer,放在 <head>
  2. 有依赖关系的多个脚本 → 全部使用 defer,按依赖顺序排列
  3. 独立的第三方脚本(统计、广告等) → 使用 async
  4. ES Module 脚本 → 默认就是 defer,不需要额外添加
  5. 避免把无属性的 <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') 创建的脚本标签,默认 asynctrue,也就是下载完立即执行,不保证顺序。如果需要按顺序执行,需要手动设置 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
}