async脚本执行顺序不可控,因“谁先下载完谁先执行”,与HTML书写顺序无关;仅适用于完全独立、不操作DOM、无依赖的脚本,如统计或广告SDK。

async脚本执行顺序根本不可控
async不保序,这是它的设计本质——谁先下载完谁先执行,和HTML里写的先后顺序完全无关。你写<script src="utils.js" async></script>在前、<script src="app.js" async></script>在后,只要app.js体积小或CDN更快,它就可能先执行,然后立刻报ReferenceError: utils is not defined。
常见错误场景包括:把埋点SDK和业务初始化都设成async;用构建工具自动注入polyfill但没控制属性;或者误以为“加了async就比同步快,顺便还能保序”。
- async只适合完全独立的脚本:比如
analytics.js、adsense.js,它们不依赖其他JS,也不操作document - 一旦脚本里有
document.getElementById、window.utils这类调用,就别用async——它可能在还没解析出来时就执行 - 多个async脚本之间无法建立依赖关系,浏览器不会等任何一个,也不会排队
想防乱序报错,就得换加载策略
async不是万能解药,遇到多依赖必须主动切换语义。核心原则是:依赖链存在,就不能靠网络快慢决定执行顺序。
- 对外部脚本,统一改用
defer:它保证按HTML书写顺序执行,且一定在DOM就绪后、DOMContentLoaded前运行,document.body可直接用 - 如果必须保留async(比如第三方统计SDK),就把业务逻辑拆出来,用
onload回调触发后续加载:s.onload = () => loadScript('app.js') - 避免混用:页面里同时存在
async和defer脚本,它们的执行队列完全隔离,async脚本可能在defer脚本开始下载前就执行完了 - 不要给
type="module"脚本加async:ESM天然具备defer行为,加async不仅冗余,还可能被Webpack等工具误判依赖
动态加载才是复杂依赖的真实解法
当依赖关系不是静态写死在HTML里(比如按用户操作加载图表库→渲染组件→请求数据),光靠async/defer已经失效。这时候得靠JS运行时控制。
立即学习“前端免费学习笔记(深入)”;
- 用
import()代替script标签:它返回Promise,天然支持链式调用,import('./utils.js').then(() => import('./app.js')) - 封装
loadScript()函数,内部用Promise+onload/onerror管理状态,失败可重试或降级 - 对非首屏模块,配合
sessionStorage记录已加载路径,防止刷新后重复执行或错序 - 慎用
document.write()动态插入script:现代浏览器基本禁用,且生成的脚本不继承async或defer语义
容易被忽略的兼容与调试细节
async在旧版IE(如IE9)中会被忽略,退化为同步加载,反而导致阻塞;而Chrome DevTools的Network面板里,async脚本的Priority常显示为Highest,可能抢占关键CSS带宽。
- 检查HTTP缓存头:如果
utils.js被强缓存但app.js没缓存,async下后者可能先加载完并执行,照样报错 - 服务端渲染(SSR)页面中,defer脚本可能在hydration完成前就执行,导致React/Vue状态不一致——这时得结合
requestIdleCallback或setTimeout延迟 - 用
link rel="preload"提前拉取脚本时,as="script"必须显式声明,漏写会导致优先级降级,预加载失效 - 所有async脚本里访问DOM前,必须加
if (document.body)兜底,不能假设DOMContentLoaded已触发



















