script放body底部最稳妥,因浏览器自上而下解析html,head中同步脚本执行时dom未生成,导致document.getelementbyid返回null;defer/async仅对外部脚本有效,内联脚本加defer无效。

script 标签放 底部最稳妥,除非你明确需要它提前执行且已用 defer 或 async 控制时机。
为什么放 会报 document.getElementById is null
浏览器解析 HTML 是严格从上到下、线性推进的。当解析器遇到 <script></script>,会立即暂停 DOM 构建,下载并执行脚本(同步行为)。此时 里的元素根本还没被解析出来,document.getElementById('xxx') 必然返回 null。
常见错误现象包括:Cannot read property 'innerHTML' of null、addEventListener is not a function(因为绑定目标是 undefined)。
即使脚本只有两行,也不代表它会“自动等 DOM 就绪”——没加任何控制,它就按顺序执行。
defer 和 async 是 里唯一安全的例外
defer 只对外部脚本(带 src 属性)生效,它让脚本异步下载,但等到 DOM 解析完成(DOMContentLoaded 前)才执行,且保持顺序。适合依赖 DOM 且需多个脚本协同的场景,比如 Vue/React 的入口初始化。
async 同样只对外部脚本有效,但下载完就立刻执行,不保证顺序,也不等 DOM。适合完全独立、无依赖的脚本,比如统计埋点、广告 SDK。
注意:defer 和 async 对内联脚本(没有 src)无效,写了也当不存在。
示例对比:
<script defer src="app.js"></script><script async src="analytics.js"></script><script>console.log(document.getElementById('nav'))</script><!-- 这行依然报 null -->
放 底部不是“万能解”,也有隐含代价
把 <script></script> 放在 前,确实能确保 DOM 就绪,但要注意三点:
- 脚本仍会阻塞后续 HTML 解析(虽然只剩闭合标签了),不过影响极小;
- 如果页面有大量内联脚本或多个
<script></script>标签,它们会按顺序串行执行,可能拖慢交互就绪时间; - 某些现代框架(如 Svelte、Qwik)依赖服务端预渲染 + 客户端 hydration,这时脚本位置需配合 hydrate 时机,不能简单“扔到底部”。
真实项目中,混合使用更常见:核心业务逻辑放底部,第三方分析脚本用 async 放 ,框架入口用 defer。
最容易被忽略的细节:内联脚本 + defer 是无效组合
很多人看到“defer 安全”,就给内联脚本也加上这个属性,结果发现还是报错。这是因为 defer 的规范定义明确要求脚本必须有 src。浏览器遇到 <script defer>xxx</script>,会直接忽略 defer,当作普通内联脚本立即执行。
如果你非要用内联方式又想延迟执行,只能手动包裹:
<script>
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('main').innerHTML = 'loaded';
});
</script>
但这样写多了,代码组织和可维护性就明显变差——这也是为什么现代项目倾向用构建工具统一处理加载时机,而不是靠手调 <script></script> 位置。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











