必须用defer:脚本需操作DOM或依赖其他脚本时,defer确保HTML解析完成、DOMContentLoaded前按序执行,DOM完整可用;async仅适用于无DOM依赖、不被依赖的独立脚本,如统计上报。

脚本加了 async 却报 document.getElementById 返回 null?不是加载慢,是执行时机错了——关键不在“要不要快”,而在“能不能碰 DOM”和“有没有依赖”。
什么时候必须用 defer
脚本要操作 DOM 或依赖其他脚本导出的变量时,defer 是唯一安全选项。
- 典型场景:Vue/React 挂载
#app、绑定按钮点击事件、调用document.querySelector获取节点、先加载lodash.js再用_.debounce -
defer脚本一定在 HTML 解析完成、DOMContentLoaded触发前执行,DOM 树完整可用 - 多个
defer脚本严格按 HTML 中出现顺序执行,适合构建依赖链 - 注意:
defer只对带src的外部脚本生效;<script defer>console.log(1)</script>中的defer被浏览器完全忽略
什么时候可以放心用 async
async 仅适用于真正自包含、不读写 DOM、也不被其他脚本依赖的脚本。
- 典型例子:
analytics.js、ad-banner.js、error-tracker.js,它们只调navigator.sendBeacon或fetch上报,失败不影响主逻辑 -
async脚本下载完立刻执行,可能发生在<body>都没解析到一半的时候,document.getElementById('main')返回null是常态 - 多个
async脚本谁先下完谁先跑,顺序不可控——所以别把jquery.js和plugin.js都设成async - 如果同时写了
<script async defer src="a.js"></script>,现代浏览器会直接忽略defer,按async行为执行
type="module" 脚本的隐式 defer 行为
ES 模块脚本天然具备 defer 特性,但有额外约束,不能简单当成“更高级的 defer”来用。
立即学习“前端免费学习笔记(深入)”;
-
type="module"脚本默认不阻塞解析、延迟执行、保序,且支持import语法 - 但模块路径必须是相对或绝对 URL,不能是纯文件名;CDN 上的模块脚本必须是 CORS 可访问的,否则静默失败
- 旧版 IE 完全不执行
type="module",需提供降级 fallback(如传统defer脚本) - 内联模块(
<script type="module">...)也受 same-origin 限制,无法跨域执行
动态插入脚本的适用场景与坑点
当脚本不该随页面一起加载,而是由用户行为或运行时条件触发时,必须用 JS 动态插入。
- 常见用例:评论区滚动到底部才加载、地图组件点击后引入、A/B 测试分流加载不同 SDK
- 关键控制点:必须手动监听
onload和onerror,否则加载失败静默,调试困难 -
script.async = false可显式关闭异步,但必须确保插入时机合理(比如插入到<head>后且 DOM 尚未开始解析) - 示例:
const s = document.createElement('script'); s.src = '/js/chart.js'; s.onload = () => initChart(); document.head.appendChild(s);—— 注意onload必须在src赋值之后设置
最常被忽略的复杂点:混合使用 defer、async 和动态脚本时,它们之间没有执行优先级约定。defer 脚本虽保证在 DOMContentLoaded 前执行,但不保证在所有 async 脚本之后——尤其当某个 async 脚本极小、极快下载完成时,它可能早于 defer 脚本执行。



















