必须用defer;document.getElementById("app")返回null是因为脚本执行过早,DOM尚未解析完成;defer保证HTML解析完毕、DOMContentLoaded前执行,此时DOM已就绪,而async下载完立即执行,可能在<body>标签解析前就运行,且内联脚本加defer无效,async与defer不可共存。

脚本要操作 DOM,必须用 defer;只上报数据或加载广告,优先用 async;二者不能共存,加了也白加。
为什么 document.getElementById("app") 总是返回 null
这不是代码写错了,大概率是脚本执行太早 —— DOM 还没解析完就去查元素。同步脚本(没加任何属性的 <script src="x.js"></script>)会立刻中断 HTML 解析,但若它放在 <head> 里,<body> 根本还没开始解析,document.body 都是 undefined。
-
defer脚本能解决这个问题:它保证脚本在 HTML 解析完成、DOMContentLoaded触发前执行,此时整个 DOM 树已就位 -
async不行:它下载完就跑,可能在<div id="app">出现前就执行了 - 内联脚本加
defer无效:<script defer>init();</script>中的defer会被浏览器忽略,只对外链脚本生效
async 和 defer 的执行顺序到底谁说了算
顺序不是靠“谁先写”决定的,而是由属性语义硬性约束的:
- 多个
defer脚本:严格按 HTML 中出现顺序执行,哪怕vue.js下载慢、app.js先下完,也得等前者执行完再轮到后者 - 多个
async脚本:谁先下载完谁先执行,完全无序;analytics.js和ads.js之间不能有依赖 -
async和defer同时出现在一个标签上:async优先级更高,defer被静默忽略 —— 浏览器按标准执行,不是 bug
Webpack/Vite 构建产物加 defer 是不是多此一举
很可能是。现代构建工具默认输出的脚本如果带 type="module",那它本身就具备 defer 语义:
-
<script type="module" src="main.js"></script>等价于加了defer,再手动加defer属于冗余配置 - 但
type="module"脚本内部用import()动态导入的模块,不受defer约束 —— 它们的加载和执行时机仍需代码自己控制 - 如果构建产物是传统 IIFE 格式(没
type="module"),那该加defer还得加,别被构建工具名头唬住
把主业务脚本设成 async 是性能雷区
常见错误:为了“快一点”,给 main.js 加 async,结果页面交互失灵、Vue 初始化失败、React 报 Root container is not found。
-
async脚本执行时会阻塞 HTML 解析(虽然不阻塞下载),而主业务脚本体积大、执行慢,反而拖累首屏渲染 - 统计类脚本(如
analytics.js)适合async:没 DOM 依赖、无其他脚本依赖、越早上报越好 - 真正需要“快”的不是执行时间,而是用户可感知的渲染完成时间 ——
defer让脚本等 DOM 就绪后再跑,反而更稳更快
最易被忽略的一点:defer 只管外链脚本的执行时机,不管它内部行为。即使加了 defer,如果脚本里用了 setTimeout、fetch 或 import(),后续逻辑依然可能在 DOM 就绪前触发 —— 这部分得靠你自己加判断或兜底。

















