技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

defer 和 async 的区别

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/14-defer和async的区别
本文目录12 个章节
  1. 一句话先说清楚
  2. 原文示意图
  3. async 和 defer 的共同点
  4. 它们最重要的区别
  5. 为什么 defer 按顺序执行,而 async 不按顺序执行?
  6. DOMContentLoaded 的关系
  7. 模块脚本的特殊规则
  8. async 和 defer 不能用于内联经典脚本
  9. 脚本放在 </body> 前还值得做吗?
  10. 动态插入的脚本
  11. 选择建议
  12. 参考资料

defer 和 async 的区别

Category(分类): JavaScript Status: 已更新

原文围绕《JavaScript 高级程序设计》中 deferasync 的区别展开。本文保留原来的加载顺序、HTML 解析和“脚本是否有依赖”这条学习路线,并按当前 HTML Standard 和浏览器实现补充经典脚本、模块脚本、DOMContentLoaded、动态插入脚本等边界。

一句话先说清楚

当浏览器解析到下面的经典外部脚本时:

<script src="script.js"></script>

如果没有 asyncdefertype="module",HTML 解析器通常会暂停,等待脚本下载并执行,然后再继续解析后面的文档。脚本的执行本身也会占用 JavaScript 执行线程,因此不要把“下载不阻塞”与“执行不阻塞”混为一谈。

1. 普通经典脚本

<script src="script.js"></script>
  • 解析器读到标签后暂停解析;
  • 外部脚本下载完成后立即执行;
  • 后面的 HTML 要等脚本执行完成才能继续解析;
  • 多个普通脚本按照它们在文档中的顺序执行。

2. async 脚本

<script async src="analytics.js"></script>
  • 下载可以与 HTML 解析并行进行;
  • 下载完成后立即执行,执行时可能暂停 HTML 解析;
  • 多个 async 脚本不保证执行顺序;
  • 不应依赖其他脚本或 DOM 已经解析完成;
  • DOMContentLoaded 不会等待 async 脚本。

3. defer 脚本

<script defer src="app.js"></script>
  • 下载可以与 HTML 解析并行进行;
  • HTML 文档解析完成后才执行;
  • 多个 defer 脚本按照文档顺序执行;
  • 执行发生在 DOMContentLoaded 之前;
  • DOMContentLoaded 会等待这些脚本下载并执行完成。

原文示意图

经典脚本、async 和 defer 的加载时序示意

配图依据 HTML Standard 的 async/defer 示意图 本地保存;MDN 页面说明其按 CC BY 4.0 条款提供。下面保留原文历史配图,当前规范结论不以它为唯一依据。

原文历史配图:脚本下载、解析和执行时序

蓝色线可以理解为脚本的网络获取,红色线表示脚本执行,绿色线表示 HTML 解析。图是历史配图,细节以当前 HTML Standard 为准;尤其是现代页面还要考虑模块脚本、预加载、网络优先级和渲染机会。

asyncdefer 的共同点

  1. 对于带 src 的经典脚本,它们都允许脚本下载与 HTML 解析并行进行;
  2. 它们都能避免“先下载脚本、再继续解析文档”的下载阶段阻塞;
  3. 它们都不能让 JavaScript 执行本身变成并行执行。脚本开始执行时,当前 JavaScript 执行仍然会占用该执行环境;
  4. 两者都只改变脚本的加载/执行时机,不会自动解决脚本之间的业务依赖。

它们最重要的区别

对比项普通经典脚本asyncdefer
下载是否可与 HTML 解析并行否(通常阻塞解析)
何时执行下载完成后立即执行下载完成后尽快执行文档解析完成后执行
多个脚本的执行顺序按文档顺序不保证,谁先下载完谁先执行按文档顺序
是否适合有依赖的脚本可按顺序依赖不适合适合按顺序依赖
是否等待 DOM 解析完成不等待
DOMContentLoaded 是否等待普通脚本会自然地阻塞解析不等待等待执行完成

因此,原文总结中的前两点仍然成立:asyncdefer 的下载都可以异步进行,区别主要在下载完成后的执行时机;如果应用脚本需要 DOM 或其他脚本,defer 通常比 async 更合适。

为什么 defer 按顺序执行,而 async 不按顺序执行?

假设页面中有两个脚本:

<script defer src="vendor.js"></script>
<script defer src="app.js"></script>

即使 app.js 先下载完成,浏览器也会等到合适的时机,并按照 vendor.jsapp.js 的文档顺序执行。这适合 app.js 依赖 vendor.js 的情况。

而下面的写法不能依赖顺序:

<script async src="vendor.js"></script>
<script async src="app.js"></script>

如果 app.js 先下载完成,它可能先执行,此时 vendor.js 暴露的全局变量还不存在。只有当两个脚本彼此独立时,async 才是合适的选择,例如独立的统计、广告或监控脚本(仍应结合隐私、性能和安全要求配置)。

DOMContentLoaded 的关系

<script defer src="a.js"></script>
<script defer src="b.js"></script>
<script async src="metrics.js"></script>
<script>
  document.addEventListener('DOMContentLoaded', () => {
    console.log('defer 脚本已经按顺序执行')
  })
</script>

这里可以确定:

  1. a.js 先于 b.js 执行;
  2. DOMContentLoaded 发生在 a.jsb.js 执行完成之后;
  3. metrics.js 可能在 DOMContentLoaded 之前执行,也可能在之后执行,事件不等待它;
  4. DOMContentLoaded 只表示文档已经解析完成,并不表示图片、样式表、字体和所有异步请求都已完成;
  5. window.load 通常比 DOMContentLoaded 更晚,并且会等待更多资源,但也不能把它当作“所有业务异步任务都完成”的信号。

defer 脚本如果下载失败或执行抛错,也不能简单认为页面会等待到业务成功;事件顺序和错误处理应通过 error 事件、监控和应用逻辑明确处理。

模块脚本的特殊规则

现代项目经常使用 ES modules:

<script type="module" src="main.js"></script>

模块脚本与经典脚本不同:

  • 模块脚本默认具有类似 defer 的非阻塞解析行为;
  • defer 对模块脚本没有额外效果;
  • 模块及其静态依赖会先被获取,再按模块依赖关系链接和评估;
  • 模块拥有自己的作用域,不会因为顶层 constfunction 自动成为 window 属性;
  • 跨源模块请求需要满足 CORS;
  • async 可以用于模块脚本,此时模块及其依赖准备好后即可评估,不保证多个模块脚本标签之间的顺序;
  • 动态 import() 会在运行时加载模块,不属于页面初始模块脚本的顺序保证;
  • 模块中的顶层 await 会让模块评估异步化。依赖模块会等待相应评估,但 DOMContentLoaded 不应被当作所有顶层 await 业务初始化都完成的通用信号;需要明确的初始化完成状态时,应显式暴露并等待一个 Promise。

例如:

<script type="module" src="main.js"></script>
<script nomodule src="legacy.js"></script>

支持模块的浏览器会忽略 nomodule 脚本,从而可以为旧浏览器提供降级版本。实际项目是否还需要 nomodule,要结合目标浏览器和构建产物决定。

asyncdefer 不能用于内联经典脚本

下面的 defer 不会把内联代码推迟到文档解析完成后:

<script defer>
  console.log('这段内联经典脚本不会因为 defer 自动延后')
</script>

asyncdefer 的这类加载语义主要针对带 src 的经典脚本。若要延后内联代码,应将代码放到 DOMContentLoaded 监听器中,或把它放进模块/外部脚本中:

<script>
  document.addEventListener('DOMContentLoaded', () => {
    initPage()
  })
</script>

不过,如果脚本只是为了访问已经在脚本后面的 DOM,通常应优先使用 defer 的外部脚本,而不是把大量代码塞进事件监听器。

脚本放在 </body> 前还值得做吗?

原文提到,把脚本放到 </body> 前是旧浏览器时代常见的优化方式。这个历史经验仍然值得理解,但不能再当成唯一最佳实践:

<body>
  <main id="app"></main>
  <script src="app.js"></script>
</body>

脚本位于文档末尾时,前面的 HTML 已经解析,脚本对首屏内容的阻塞影响可能较小;但现代页面通常更适合:

<head>
  <script defer src="app.js"></script>
</head>

这样可以尽早发现并下载脚本,同时不阻塞 HTML 解析,并保持有序执行。构建工具还可能使用 <link rel="modulepreload">、资源优先级提示和代码分割来优化关键路径,不能只靠移动标签位置判断性能。

动态插入的脚本

通过 JavaScript 创建的经典脚本默认具有异步加载倾向:

const script = document.createElement('script')
script.async = false // 多个动态经典脚本可按插入顺序执行
script.src = '/feature.js'
script.onload = () => console.log('feature loaded')
script.onerror = () => console.error('feature failed')
document.head.append(script)

动态脚本不能直接套用页面中两个 defer 标签的顺序保证。经典动态脚本默认倾向于异步执行;设置 script.async = false 可以请求按插入顺序执行,但如果业务依赖明确,仍建议使用 Promise 加载器,让后一个加载依赖前一个完成,而不是依赖网络时序:

function loadScript(src) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script')
    script.src = src
    script.onload = resolve
    script.onerror = reject
    document.head.append(script)
  })
}

async function loadFeatures() {
  await loadScript('/vendor.js')
  await loadScript('/app.js')
}

loadFeatures()

选择建议

适合 async 的场景

  • 脚本与页面主逻辑无依赖;
  • 脚本与其他脚本无顺序依赖;
  • 脚本下载完成后尽快执行即可;
  • 例如独立统计、某些第三方 SDK 或延迟可选功能。

适合 defer 的场景

  • 脚本需要访问页面 DOM;
  • 多个脚本有先后依赖;
  • 希望脚本不阻塞 HTML 解析;
  • 希望在 DOMContentLoaded 前完成初始化。

需要额外考虑的事项

  • 对关键脚本设置正确的缓存策略和压缩格式;
  • 第三方脚本使用 async 并不代表没有隐私或供应链风险;
  • 可以使用 integrity(子资源完整性)和 CSP 限制脚本来源;
  • 不要因为脚本“异步下载”就把巨大的同步执行逻辑放在首屏关键路径上;
  • 对非关键功能优先考虑动态 import() 和代码分割。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS