js文件中async和defer属性的区别及加载顺序
深入解析HTML中script标签的async与defer属性的区别、执行时机与多脚本加载顺序,结合完整流程图、时间线图与工程实战场景,帮助开发者正确选择脚本加载策略,避免页面渲染卡顿与依赖错误。
<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 这两个布尔属性正是为此而生。
二、核心属性工作机制与横向对比
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加载成功,整个页面的核心功能也已崩溃。
document.createElement('script') 并插入 DOM,浏览器默认会将 script.async 设置为 true!这意味着动态加载的多个脚本默认是乱序执行的。如果你需要动态插入有依赖顺序的脚本链,必须手动显式声明 script.async = false;,强制浏览器保持添加顺序执行。
四、实际应用场景与工程选型架构
在实际项目搭建中,并不是简单地给所有脚本无脑添加某一个属性,而是需要根据脚本在应用架构中的角色分门别类地应用。
1. 必须优先使用 defer 的典型场景
-
依赖 DOM 元素的业务脚本:
如果你的脚本需要操作页面中的 DOM 元素(例如
document.getElementById('root')、挂载轮播图、绑定事件),defer保证在脚本执行时,整个 HTML 树已经完整构建,无需再在代码内包裹冗余的window.onload或document.addEventListener('DOMContentLoaded', ...)。 -
存在依赖关系的多文件组合:
例如传统的
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 的独立场景
-
独立第三方数据统计与埋点:
如百度统计、Google Analytics、友盟统计等。这类脚本不依赖页面其他业务逻辑,也不被其他代码调用,它唯一的目标就是尽快就绪开始收集访问数据。使用
async能让统计脚本在下载完的第一时间静默激活。 -
线上异常监控与性能埋点:
例如 Sentry 错误上报、Web Vitals 性能探针。它们希望尽早介入生命周期,即使在页面未完全解析时捕获早期的报错,此时
async能够提供最敏捷的拉取与执行速度。 - 广告投放与独立社交挂件: 第三方广告 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的抢跑执行模式。
五、开发中不可忽视的四个注意事项
-
只对外链脚本生效:
defer与async属性仅对带有src属性的外部脚本文件生效。对于写在标签体内的内联脚本(Inline Script),浏览器直接忽略这两个属性,仍会按默认方式同步解析阻塞。 -
同时声明两个属性时的降级行为:
如果在一个
<script>标签上同时写了async和defer(例如<script src="app.js" async defer>),现代浏览器会优先遵循 async 规则;只有在极老旧不支持async但支持defer的浏览器中,才会自动回退降级为defer模式。 -
禁止在 defer / async 脚本中使用 document.write():
由于异步脚本执行时,HTML 解析器要么在后台并行,要么已经关闭构建,调用
document.write()会导致严重的副作用,现代浏览器通常会直接忽略该方法调用或触发控制台警告。 -
慎在 async 脚本中操作首屏关键 DOM:
因为
async脚本执行时 DOM 树处于半成品状态,查询节点极易返回null。如果必须在async脚本中操作页面元素,必须内部监听DOMContentLoaded事件,而这又变相抵消了async的早期执行优势。
六、工程选型决策路径与快速速查
为了在实际开发中快速作出准确判断,可以按照下方决策树进行逐级筛选:
在日常前端工程中,90% 以上的业务脚本都应当优先选用 defer 并放置在 <head> 中。这样既能利用 <head> 极早触发网络并发下载,又能确保在安全稳定的 DOM 环境下按部就班地执行业务代码;仅在面对无依赖的第三方埋点统计、异常监控和独立广告脚本时,选用 async 达成最大化的加载性能。
前端页面怎么实现JSON数据的高亮格式化展示
2026-11-05 18:12:52
怎么使用 JavaScript import() 实现模块的条件加载
2026-11-05 14:44:45
前端防御XSS攻击:怎么防止恶意JS注入与CSP策略配置
2026-11-04 08:51:38
怎么解决 Turf.js 处理大规模 GeoJSON 数据的性能瓶颈
2026-11-03 23:22:19
为什么不建议在JS中手动拼接JSON字符串?
2026-11-03 22:48:27








