脚本放<head>中未加defer/async会导致document.getElementById返回null,因浏览器流式解析时遇script即暂停DOM构建,此时<body>尚未解析;defer确保外部脚本在DOM构建完成、DOMContentLoaded前按序执行,async则一下载完立即执行且不保序。

脚本放错位置不是“页面慢一点”的问题,而是直接导致 document.getElementById 返回 null、事件绑定失效、首屏白屏——根源在浏览器流式解析 HTML 的机制。
为什么 <script> 放 <head> 里会找不到 DOM 元素
浏览器从上到下解析 HTML,遇到没加 defer 或 async 的 <script src="app.js">,立刻暂停 DOM 构建,去下载、解析、执行 JS。此时 <body> 还没开始解析,document.getElementById("main") 当然返回 null。
- 典型报错:
Cannot set properties of null、undefined is not an object - 哪怕脚本只有一行
console.log("hi"),也会卡住整个页面渲染 - Chrome DevTools 中能看到
DOMContentLoaded时间远晚于first-contentful-paint
defer 和 async 到底该用哪个
两者都只对带 src 的外部脚本生效,行为差异极大,选错就出错。
-
defer:脚本并行下载,不阻塞 HTML 解析;等 DOM 构建完、DOMContentLoaded触发前,严格按 HTML 中出现顺序执行。适合主业务逻辑,比如utils.js必须在app.js前加载 -
async:脚本异步下载,一下载完就立刻执行,不管 DOM 是否就绪、也不管其他脚本顺序。只适合完全独立的脚本,如analytics.js、广告 SDK - 把
async脚本放在</body>前,不代表它会“等页面加载完”——它仍可能比<header>还早执行,document.querySelector("header")返回null -
type="module"脚本默认自带defer行为,还支持顶层await和静态导入分析,现代项目优先用它
放在 </body> 前就万事大吉?别太乐观
把 <script src="app.js"> 移到 </body> 前,能避开“DOM 未就绪”问题,但仍有隐患:
立即学习“前端免费学习笔记(深入)”;
- JS 文件体积大或网络慢时,
DOMContentLoaded仍会被延迟,影响交互就绪时间 - 多个脚本之间有依赖,光靠位置无法保证执行顺序(比如
vendor.js必须先于app.js) - 更稳妥的做法是:保留底部位置 + 显式加
defer,或改用type="module" - 不要混用:
async脚本不该放在底部——它会跳过顺序约束,反而更容易出错
动态加载不是靠挪位置解决的
调整 <script> 位置只能优化初始加载路径,解决不了“点击按钮才需要 ECharts”这类按需场景。
- 真正按需加载必须用运行时动态导入:
await import('https://cdn.jsdelivr.net/npm/echarts@5.4.3/+esm') - 内联脚本(没
src)加defer会被忽略,不起作用 - 动态创建的
script(如document.createElement('script'))默认行为类似async,执行时机不确定
最容易被忽略的一点:即使你把所有脚本都挪到 </body> 前,如果其中某个脚本内部用了 setTimeout 或 Promise.then,它依然可能在 DOM 就绪后很久才真正操作节点——执行时机和 DOM 就绪时间不是一回事。



















