defer/async 区别
defer 与 async 的区别在于:普通 script 阻塞解析,async 下载不阻塞但下载完立即执行且顺序不定,defer 下载不阻塞并在 DOM 解析后、DOMContentLoaded 前按顺序执行;独立脚本用 async,依赖
学完这篇你能搞清楚 defer 和 async 到底在什么时候下载、什么时候执行、谁保证顺序,并且能对着 Chrome DevTools 的 Network 面板自己验证一遍,最后知道什么场景该用哪个。
先给一个结论式的概览,再逐条拆开讲:
| 写法 | 是否阻塞解析 | 执行时机 | 执行顺序 |
|---|---|---|---|
<script src> | 阻塞 | 下载完立刻执行 | 按出现顺序 |
<script src async> | 不阻塞 | 下载完立刻执行(可能打断解析) | 不保证 |
<script src defer> | 不阻塞 | DOM 解析完成后、DOMContentLoaded 之前 | 按出现顺序 |
下面按步骤把这三点掰开。
第一步:先看清浏览器遇到普通 <script> 干了什么
这一步要确定基准行为,后面两个属性都是相对它来说的。
浏览器解析 HTML 时是一行行往下走的,遇到 <script src="a.js"> 会立刻停下来:发起请求、等下载完成、执行这段 JS、然后才继续解析后面的 HTML。
也就是说,一个 3 秒才返回的脚本,会让后面所有 HTML 的解析推迟 3 秒。用户看到的就是长时间白屏。这是引入 defer/async 的全部动机。
注意:不是"下载"本身阻塞解析,而是浏览器会暂停解析去等脚本下载+执行。现代浏览器有预加载扫描器(preload scanner)会提前发现后面的资源并并行下载,但这不改变脚本执行对 HTML 解析的阻塞。
第二步:async —— 下载不阻塞,执行随时插队
这一步要理解 async 的核心特征:下载并行,执行时打断解析,顺序随机。
写法:
<script src="a.js" async></script>
<script src="b.js" async></script>
浏览器的行为是:
- HTML 继续正常解析,不停下来;
a.js、b.js同时开始下载;- 谁先下载完,谁立刻执行,执行期间 HTML 解析暂停;
- 两者执行顺序完全取决于网络速度,可能是 b 先于 a。
所以 async 适合彼此独立、不依赖 DOM、也不依赖其他脚本的文件,典型场景是统计埋点、广告 SDK、A/B 测试脚本。
注意:
async不保证顺序,这点在真实项目里踩坑很常见。如果你有两个文件jquery.js和plugin.js,后者依赖前者,两个都写async,偶尔就会报$ is not defined,而且只在网络慢的时候复现,非常难查。
第三步:defer —— 下载不阻塞,全部解析完再按顺序执行
这一步要理解 defer 的核心特征:下载并行,等 HTML 解析完才执行,顺序严格保留。
写法:
<script src="a.js" defer></script>
<script src="b.js" defer></script>
行为是:
- HTML 继续解析,
a.js、b.js并行下载; - 即使
b.js先下载完,也不执行,等a.js下载完; - 等到文档解析结束(
</html>处理完),按a.js→b.js的顺序依次执行; - 执行完之后才触发
DOMContentLoaded。
因此用 defer 的脚本里可以放心操作 DOM,不需要套 DOMContentLoaded:
<script src="main.js" defer></script>
// main.js 里可以直接这么写
document.querySelector('#app').textContent = '加载完成';
注意:
DOMContentLoaded会等所有defer脚本执行完才触发,但不会等async脚本。如果你在DOMContentLoaded里依赖某个async脚本挂到window上的变量,就可能拿到undefined。反过来,load事件(含图片等资源)会等async脚本。
第四步:动手做一次可复现的对比实验
这一步要拿到自己眼睛能看到的证据,而不是背结论。
新建一个 HTML,把下面三个脚本按顺序放到 <head> 里,服务端故意把 /slow.js 延迟 2 秒返回:
<head>
<script src="/slow.js"></script>
<script src="/a.js" async></script>
<script src="/b.js" defer></script>
</head>
<body>
<p id="msg">正文开始</p>
<script>
console.log('inline', document.readyState);
document.addEventListener('DOMContentLoaded', () => console.log('DOMContentLoaded'));
window.addEventListener('load', () => console.log('load'));
</script>
<!-- 后面塞大量 DOM 节点,拉长解析时间 -->
</body>
用 Node 起个带延迟的静态服务即可,比如 Express:
const express = require('express');
const app = express();
app.use(express.static('public'));
app.get('/slow.js', (req, res) => setTimeout(() => res.type('js').send('console.log("slow")'), 2000));
app.listen(3000);
打开 Chrome(本文基于 Chrome 120 及以上,行为一致),按 F12 打开 DevTools:
- 切到 Network 面板,勾上 Disable cache(禁用缓存),Network 面板顶部筛选栏选 JS;
- 刷新页面,看 Waterfall 列:
slow.js的请求条会很长,a.js、b.js的请求几乎同时开始,但它们和 HTML 的解析是重叠的; - 切到 Console 面板看打印顺序。
你会看到:slow.js 里的 console.log 最早出现(因为它阻塞解析),a.js 什么时候打完全看网络,b.js 一定在文档解析结束后、DOMContentLoaded 之前打印。
注意:本地开发服务器返回太快,
async的乱序现象几乎观察不到。想复现顺序问题,必须人为给其中一个文件加延迟(上面/slow.js的做法),或者用 DevTools 的 Network Throttling 选 Slow 3G。
第五步:记清三个属性的准确边界
这一步解决"写了没生效"的问题。
- 只对外部脚本有效。
<script async>console.log(1)</script>这种内联写法,async和defer都会被浏览器直接忽略。因为它们本来就是用来控制"外部文件的下载"的,内联代码不需要下载。 - 同时写两个,
async优先。<script src="x.js" async defer>在不支持async的老浏览器里退化成defer,在现代浏览器里按async执行。 - 动态插入的脚本默认就是 async 行为。
document.createElement('script')插进文档的脚本,默认script.async === true,即下载完立刻执行、不保证顺序。想让它按插入顺序执行,要手动设置script.async = false。 type="module"默认带defer。<script type="module"> 天生就是延迟执行的,且天然按顺序,不需要你再写defer;如果加了async,就变成下载完立刻执行。
注意:老版本 IE(IE9 及以下)对
defer的支持有缺陷,会出现执行时机错乱甚至不执行。现在前端项目基本不再考虑,但如果要兼容极老环境,defer反而是风险项。
第六步:按场景做选型
这一步给你可以直接落地的判断标准。
用 defer 的情况(绝大多数业务脚本):
- 脚本要操作 DOM;
- 多个脚本之间有依赖,比如工具库在前、业务代码在后;
- 想尽量不拖慢首屏解析,又不想自己管理执行时机。
用 async 的情况:
- 脚本完全独立,不碰 DOM,也不被别的脚本依赖;
- 早点晚点执行都无所谓,但希望尽快开始;
- 典型:统计、监控、广告、第三方客服组件。
什么都不加的情况:
- 脚本必须在后面的 HTML 解析之前就生效,比如给老浏览器打补丁、做重定向、或者在 HTML 中出现之前就注入全局变量。
一个务实的默认策略:业务入口脚本一律 defer,第三方统计类脚本一律 async。
第七步:几个容易翻车的细节
defer脚本的执行顺序是按它们在 HTML 中出现的顺序,不是下载完成的顺序。所以不要把defer脚本写到<body>底部又指望它比<head>里的defer脚本先执行。async脚本执行时会阻塞解析,如果它执行时间很长(比同步跑一个大循环),首屏照样会被拖慢。async只解决"下载不阻塞",不解决"执行不阻塞"。- 用
document.write()的脚本(部分老广告代码)配合async/defer会报浏览器警告并被忽略,这类脚本只能同步引入。 - 想更进一步减少阻塞,可以配合
<link rel="preload" as="script">提前拉取关键脚本,但注意preload只负责下载,不负责执行,执行时机还是由defer/async决定。
小结
- 普通
<script>:下载和执行都阻塞 HTML 解析,顺序严格按出现顺序。 async:下载不阻塞解析,下载完立刻执行(执行时仍会打断解析),多个async脚本之间不保证顺序。defer:下载不阻塞解析,等文档解析完成后按出现顺序依次执行,执行完才触发DOMContentLoaded。- 两者都只对外部脚本(带
src)有效,内联脚本写了无效;同时写两个时async生效。 type="module"默认就是defer行为;动态插入的<script>默认是async行为,需要顺序时设script.async = false。- 选型口诀:依赖 DOM、依赖彼此 →
defer;完全独立、越早越好 →async;必须抢在解析前 → 什么都不加。
原文链接:https://www.gj0.com/thread-1810.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。