⭐ 推荐:社区规则条款 V1.0

defer/async 区别

周末
周末 新人站长
发布于 2026-10-13 06:50 ·4 浏览 ·0 回复
内容摘要

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>

浏览器的行为是:

  1. HTML 继续正常解析,不停下来;
  2. a.js、b.js 同时开始下载;
  3. 谁先下载完,谁立刻执行,执行期间 HTML 解析暂停;
  4. 两者执行顺序完全取决于网络速度,可能是 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>

行为是:

  1. HTML 继续解析,a.js、b.js 并行下载;
  2. 即使 b.js 先下载完,也不执行,等 a.js 下载完;
  3. 等到文档解析结束(</html> 处理完),按 a.js → b.js 的顺序依次执行;
  4. 执行完之后才触发 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:

  1. 切到 Network 面板,勾上 Disable cache(禁用缓存),Network 面板顶部筛选栏选 JS;
  2. 刷新页面,看 Waterfall 列:slow.js 的请求条会很长,a.js、b.js 的请求几乎同时开始,但它们和 HTML 的解析是重叠的;
  3. 切到 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;必须抢在解析前 → 什么都不加。
版权声明:本文来自 GJ站长论坛《defer/async 区别》
原文链接:https://www.gj0.com/thread-1810.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。

全部回复 0

还没有回复,来抢沙发~