document.getElementById报错主因是脚本执行时机过早,应将script移至</body>前并加defer,或改用type="module";必须放<head>的脚本需依功能选async/defer/module;按需加载须用动态import()。

document.getElementById 报错,八成是 script 放错了位置
浏览器从上到下解析 HTML,遇到没加 defer 或 async 的 <script src="app.js">,立刻暂停 DOM 构建去下载、执行。此时 <body> 还没开始解析,document.getElementById("main") 必然返回 null,接着就报 Cannot set properties of null。
这不是代码写错了,是执行时机错了。常见现象包括:
- 控制台报错但页面看起来“好像能用”——其实是部分逻辑没跑起来
-
DOMContentLoaded触发时间远晚于first-contentful-paint(Chrome DevTools 可验证) - 内联脚本(没
src)加了defer会被忽略,完全不起作用
放在 </body> 前 ≠ 万事大吉
把 <script src="app.js"> 移到 </body> 前,确实能避开 DOM 未就绪问题,但仍有隐患:
- JS 文件体积大或网络慢时,
DOMContentLoaded仍被延迟,影响用户可交互时间 - 多个脚本有依赖(比如
vendor.js必须在app.js前),光靠位置无法保证顺序 - 把
async脚本也塞到底部,反而破坏其“一下载完就执行”的语义,容易引发竞态
更稳妥的做法是:保留底部位置 + 显式加 defer,或直接改用 type="module"(它默认带 defer 行为,还支持顶层 await)。
立即学习“前端免费学习笔记(深入)”;
必须放 <head> 的脚本,只靠位置不够,得靠属性
第三方统计、Polyfill、首屏关键逻辑这些脚本确实得提前加载,但不能裸放 <head>。必须配合属性控制执行时机:
-
async:只适合完全独立的脚本,比如analytics.js;它下载不阻塞,但执行时机不可控,document.querySelector("header")可能还是null -
defer:只对外部脚本生效,下载不阻塞,执行严格按 HTML 中出现顺序,在 DOM 解析完、DOMContentLoaded前运行;适合utils.js→app.js这类有依赖的链路 -
type="module":现代首选,自带defer,静态分析导入关系,天然支持依赖顺序和错误隔离
别把 async 和 defer 混着用在同一组脚本里——它们的执行模型完全不同,混用等于主动引入不确定性。
动态加载不是挪位置能解决的
调整 <script> 位置只能优化初始加载路径,解决不了“点击按钮才需要 ECharts”这种场景。
真正按需加载必须用运行时动态导入:
await import('https://cdn.jsdelivr.net/npm/echarts@5.4.3/+esm')
注意:import() 是 Promise,必须 await 或 .then();它不依赖 HTML 位置,也不受 defer/async 影响,是唯一能解耦“触发时机”和“加载时机”的方式。
最容易被忽略的一点:哪怕你把 <script> 写在 </body> 之后(即 </body> 和 </html> 之间),HTML 解析器也会自动把它“纠正”进 <body> 内部——看似没区别,但不符合规范,审查工具可能警告,团队协作时也容易引发歧义。



















