脚本放错位置会导致document.getElementById报错、事件绑定失效、首屏白屏,根源在于浏览器流式解析HTML;未加defer/async的script在head中会阻塞DOM构建,使DOM元素不可访问。

脚本放错位置不是“页面慢一点”的问题,而是直接导致 document.getElementById 报错、事件绑定失效、首屏白屏——根源在于浏览器解析 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 - 内联脚本(没
src)加defer会被忽略,不起作用
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') - CDN 提供的 ES 模块地址必须带
+esm后缀,否则可能返回 CommonJS 格式,浏览器报错Cannot use import statement outside a module - 动态
import()返回 Promise,记得用try/catch处理加载失败 - 脚本位置再怎么调,也替代不了
import()的懒加载能力
最易被忽略的点:不是“脚本放哪儿”,而是“脚本何时执行、是否依赖其他脚本、是否操作 DOM”。defer 不是万能解药,async 也不是性能银弹,关键看脚本行为本身是否匹配加载策略。



















