js文件中async和defer属性的区别及加载顺序

作者:巡夜人

时间:2026-11-05

浏览:126

深入解析HTML中script标签的async与defer属性的区别、执行时机与多脚本加载顺序,结合完整流程图、时间线图与工程实战场景,帮助开发者正确选择脚本加载策略,避免页面渲染卡顿与依赖错误。

文章目录
分类:Web前端开发 技术要点:script属性 / 浏览器渲染管线 / 异步加载 阅读时长:约 8 分钟
js文件中async和defer属性的区别及加载顺序
在 HTML 页面中引入外部 JavaScript 文件时,<script> 标签的 defer 和 async 属性用来避免脚本下载阻塞 DOM 解析,二者的核心区别在于执行时机与多脚本执行顺序:defer 属性在后台并行下载,保证脚本在 HTML 解析完毕后、按照标签在文档中出现的先后顺序依次执行;而 async 属性同样在后台下载,但一旦文件拉取完成便立刻中断 HTML 解析就地执行,多个带有 async 的脚本谁先下载完就谁先执行,无法保证原文档书写顺序。

一、为什么需要 defer 与 async:浏览器默认机制分析

当浏览器解析 HTML 文档构建 DOM 树时,一旦在 <head> 或 <body> 中遇到普通的 <script src="main.js"></script> 标签,渲染引擎会立即停下当前的 HTML 解析工作。

此时浏览器必须先发起网络请求下载该脚本文件,等待下载完成后再交由 JavaScript 引擎完成语法解析与执行,直到脚本执行完毕,HTML 解析器才会恢复继续向下解析。这种“下载 + 执行”双重暂停机制被称为解析阻塞(Parser Blocking)。如果把体积庞大或者处于慢速网络中的脚本放在 <head> 内,用户就会看到长时间的页面白屏,导致首屏加载速度(First Contentful Paint)大幅恶化。

为了解决阻塞问题,HTML4 时代传统的权宜做法是将 <script> 标签贴在 </body> 闭合标签之前。虽然这保证了 DOM 骨架先行解析,但浏览器无法在解析 <head> 阶段提前并行下载脚本,白白浪费了宝贵的网络空闲窗口。defer 与 async 这两个布尔属性正是为此而生。

js文件中async和defer属性的区别及加载顺序
图 1:三种脚本引入方式在 HTML 解析、脚本下载及执行过程中的时间轴与阻塞对比

二、核心属性工作机制与横向对比

defer 和 async 都属于外部脚本(带有 src 属性)的布尔属性,两者都会告诉浏览器:“请开启后台异步网络线程下载该脚本,不要停下手中的 HTML 解析”。但后台下载完成之后,处理策略截然不同。

1. defer 属性(延迟执行)

  • 下载过程:浏览器在解析 HTML 的同时,在后台并发拉取脚本资源,HTML 解析全程不受影响。
  • 等待触发:脚本即使很早就下载好了,也不会立即执行,而是静默等待整个 HTML 文档解析完成。
  • 执行时机:当 HTML 解析器构建完整个 DOM 树后、DOMContentLoaded 事件触发之前,浏览器会集中执行带有 defer 的脚本。
  • 保序性:严格按照脚本在 HTML 源码中声明的物理先后顺序依次执行。

2. async 属性(独立异步执行)

  • 下载过程:与 defer 相同,浏览器会在后台发起异步下载,不会阻断当前文档解析。
  • 执行时机:一旦某个脚本通过网络下载完毕,浏览器会立即暂停当前的 HTML 解析(如果此时仍在解析中),直接切换到 JavaScript 引擎去执行该脚本;执行完毕后,再恢复 HTML 解析。
  • 乱序性:不保证先后顺序。各个 async 脚本完全根据网络传输耗时“抢跑”,体积小或网络缓存命中的脚本会率先触发执行。
对比维度 默认 <script> <script defer> <script async>
网络下载时机 遇到标签即刻同步阻塞拉取 后台并行异步下载(不阻塞解析) 后台并行异步下载(不阻塞解析)
脚本执行时机 下载完成立刻同步执行并阻塞 DOM解析完毕后、DOMContentLoaded 前 只要下载完毕即刻暂停解析执行
多脚本执行顺序 严格按文档顺序 严格按文档出现顺序 无序(先下载完成先执行)
与 DOMContentLoaded 关系 在其触发之前完成 必定在事件触发前执行完毕 无确定关系(可在其前或其后)
首屏白屏影响 严重阻断首屏内容呈现 极低,不干扰骨架生成 低(仅在执行短暂切片时暂停解析)

三、加载顺序与执行时机推导过程

很多工程 Bug 的产生,并不是因为单个脚本加载慢,而是因为多个脚本之间存在调用依赖时,执行顺序发生了紊乱。下面通过一个具体的推导过程,演示多个脚本在不同属性下的执行行为。

假设在页面 <head> 中依次声明了两个脚本:基础框架库 core.js(体积 200KB,网络传输耗时约 350ms)和依赖基础库的业务代码 business.js(体积 20KB,网络传输耗时仅需 80ms)。

<!-- 示例代码:两个具备依赖关系的脚本 -->
<script src="https://cdn.example.com/core.js" [属性]></script>
<script src="https://cdn.example.com/business.js" [属性]></script>

我们对比两种属性下的运行轨迹:

  • 使用 defer 时(推导路径): 浏览器同时向 CDN 建立连接下载两个文件。80ms 时,体积较小的 business.js 已经下载完成存入内存;但由于声明了 defer,规范要求它必须排入延迟队列等待。到了 350ms 时,core.js 也下载完成,并且 HTML 解析完成。此时,浏览器唤醒执行队列,严格按照 HTML 声明顺序:先执行 core.js(注入全局框架对象),再执行 business.js。业务代码顺利读取到框架变量,随后派发 DOMContentLoaded。整个流程稳定可控。
  • 使用 async 时(推导路径): 浏览器同样在 0ms 启动并行拉取。在第 80ms 时,体积小巧的 business.js 率先到达。由于 async 要求“下载完成即刻执行”,浏览器毫不犹豫地暂停解析,就地启动执行 business.js。然而,此时 core.js 还在网络管道中传输,全局对象尚未就绪,控制台瞬间抛出致命错误:Uncaught ReferenceError: CoreFramework is not defined。随后哪怕 core.js 加载成功,整个页面的核心功能也已崩溃。
js文件中async和defer属性的区别及加载顺序
图 2:多脚本加载时的等待队列与竞争竞态推导流程
避坑提醒:动态创建脚本的默认行为
在现代浏览器中,如果使用 JavaScript 动态创建 document.createElement('script') 并插入 DOM,浏览器默认会将 script.async 设置为 true!这意味着动态加载的多个脚本默认是乱序执行的。如果你需要动态插入有依赖顺序的脚本链,必须手动显式声明 script.async = false;,强制浏览器保持添加顺序执行。

四、实际应用场景与工程选型架构

在实际项目搭建中,并不是简单地给所有脚本无脑添加某一个属性,而是需要根据脚本在应用架构中的角色分门别类地应用。

js文件中async和defer属性的区别及加载顺序
图 3:前端工程架构中的脚本依赖关系与属性映射矩阵

1. 必须优先使用 defer 的典型场景

  1. 依赖 DOM 元素的业务脚本: 如果你的脚本需要操作页面中的 DOM 元素(例如 document.getElementById('root')、挂载轮播图、绑定事件),defer 保证在脚本执行时,整个 HTML 树已经完整构建,无需再在代码内包裹冗余的 window.onload 或 document.addEventListener('DOMContentLoaded', ...)。
  2. 存在依赖关系的多文件组合: 例如传统的 jquery.min.js 之后紧跟 jquery.plugin.js,或者 common.js 之后紧跟 page-main.js。使用 defer 可以放心写在 <head> 中,享受并行下载加速的同时,保持依赖顺序万无一失。
<!-- 推荐实践:业务依赖脚本使用 defer 置于 head -->
<head>
  <meta charset="UTF-8">
  <title>电商商品详情页</title>
  <!-- 基础依赖先引入 -->
  <script src="/assets/js/vendor-bundle.js" defer></script>
  <!-- 业务脚本依赖 vendor,保证在其后执行 -->
  <script src="/assets/js/product-detail.js" defer></script>
</head>

2. 最适合使用 async 的独立场景

  1. 独立第三方数据统计与埋点: 如百度统计、Google Analytics、友盟统计等。这类脚本不依赖页面其他业务逻辑,也不被其他代码调用,它唯一的目标就是尽快就绪开始收集访问数据。使用 async 能让统计脚本在下载完的第一时间静默激活。
  2. 线上异常监控与性能埋点: 例如 Sentry 错误上报、Web Vitals 性能探针。它们希望尽早介入生命周期,即使在页面未完全解析时捕获早期的报错,此时 async 能够提供最敏捷的拉取与执行速度。
  3. 广告投放与独立社交挂件: 第三方广告 SDK、在线客服浮窗等与首屏核心操作无关的挂件。
<!-- 推荐实践:独立统计与监控使用 async -->
<head>
  <!-- 网站流量监控:独立工作,谁先到达谁先执行 -->
  <script src="https://hm.baidu.com/hm.js?xxxxxxxx" async></script>
  <!-- 线上性能监控探针 -->
  <script src="https://browser.sentry-cdn.com/bundle.min.js" async></script>
</head>

3. ES6 模块脚本(type="module")的特殊情况

现代前端项目中广泛使用原生 ES 模块(ESM)。当在 <script> 上声明 type="module" 时:

  • 默认具备 defer 行为:即使不写 defer 属性,外部模块脚本也会自动异步下载,并延迟到 DOM 解析完成后、按模块依赖树与文档顺序保序执行。
  • 显式声明 async:如果给 <script type="module" src="..." async> 加上了 async,模块脚本及其依赖项下载完毕后会立刻无序执行,蜕变为 async 的抢跑执行模式。

五、开发中不可忽视的四个注意事项

  1. 只对外链脚本生效: defer 与 async 属性仅对带有 src 属性的外部脚本文件生效。对于写在标签体内的内联脚本(Inline Script),浏览器直接忽略这两个属性,仍会按默认方式同步解析阻塞。
  2. 同时声明两个属性时的降级行为: 如果在一个 <script> 标签上同时写了 async 和 defer(例如 <script src="app.js" async defer>),现代浏览器会优先遵循 async 规则;只有在极老旧不支持 async 但支持 defer 的浏览器中,才会自动回退降级为 defer 模式。
  3. 禁止在 defer / async 脚本中使用 document.write(): 由于异步脚本执行时,HTML 解析器要么在后台并行,要么已经关闭构建,调用 document.write() 会导致严重的副作用,现代浏览器通常会直接忽略该方法调用或触发控制台警告。
  4. 慎在 async 脚本中操作首屏关键 DOM: 因为 async 脚本执行时 DOM 树处于半成品状态,查询节点极易返回 null。如果必须在 async 脚本中操作页面元素,必须内部监听 DOMContentLoaded 事件,而这又变相抵消了 async 的早期执行优势。

六、工程选型决策路径与快速速查

为了在实际开发中快速作出准确判断,可以按照下方决策树进行逐级筛选:

js文件中async和defer属性的区别及加载顺序
图 4:JavaScript 外部脚本引入方式工程选型决策树与一句话总结
总结归纳

在日常前端工程中,90% 以上的业务脚本都应当优先选用 defer 并放置在 <head> 中。这样既能利用 <head> 极早触发网络并发下载,又能确保在安全稳定的 DOM 环境下按部就班地执行业务代码;仅在面对无依赖的第三方埋点统计、异常监控和独立广告脚本时,选用 async 达成最大化的加载性能。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章