async脚本执行时dom可能不存在,因下载完即执行而中断html解析;defer脚本则保证在dom解析完成、domcontentloaded前执行,严格按书写顺序且可安全访问完整dom。

async 脚本执行时 DOM 可能根本不存在
这是最常踩的坑:用 async 加载初始化逻辑,却在脚本里写 document.getElementById("app"),结果返回 null。不是代码写错了,是执行时机太早——async 脚本一下载完就中断 HTML 解析去执行,此时 都可能还没解析到。
- 典型错误现象:
TypeError: Cannot read property 'addEventListener' of null - 哪怕脚本放在
末尾,也无法保证<div id="app"> 已存在 <li> <code>window.addEventListener('DOMContentLoaded', ...)在async脚本里是冗余的,它自己可能比该事件早得多,也可能晚得多 - 多个
defer脚本严格按 HTML 中书写顺序执行,哪怕b.js比a.js下得快,也必须等a.js执行完 - 只对带
src的外部脚本生效;<script defer>init()</script>中的defer被忽略,init()会立刻运行 - Webpack/Vite 构建产物若已用
type="module",再加defer实际无效——模块脚本默认具defer语义 - 这不是兼容性问题,是 HTML 规范明确定义的优先级规则
- 服务端渲染(SSR)页面中,把初始化逻辑塞进
里的同步脚本再加defer,同样无效——因为没src,属性被忽略 - 想保序又想异步?只能选
defer;想尽快执行且无依赖?才用async - HTTP/1.1 下受队头阻塞影响明显;HTTP/2 虽缓解下载,但执行阻塞仍在
- 放在
里的同步脚本危害最大,用户可能长时间面对空白页 - 把脚本挪到
前是兜底方案,但不如defer利用网络并行下载来得高效
defer 脚本一定能安全访问完整 DOM
defer 的执行时机是浏览器强制保证的:HTML 解析完成、DOMContentLoaded 触发前。此时 document.body、document.head 和所有已声明的元素都可用,不是“大概率”,而是规范级承诺。
async 和 defer 同时出现时 defer 被忽略
写成 <script async defer src="a.js"></script> 不是“更保险”,而是明确放弃顺序保障。async 优先级高于 defer,所有现代浏览器(IE10+、Chrome、Firefox、Safari)都会直接忽略 defer,按 async 行为处理。
不加属性的 script 是最大性能隐患
默认行为是同步阻塞:遇到 <script src="x.js"></script>,HTML 解析立刻暂停,等 JS 下载、编译、执行完才继续。首屏白屏时间直接受网络延迟和脚本体积影响,Lighthouse 性能分暴跌。











