现代主业务脚本应放<head>并用type="module"或defer,因默认延迟执行且DOM可用;async仅适用于完全独立的统计等脚本;polyfill需defer早下载晚执行;内联脚本不支持defer,应避免使用。

脚本放在哪儿,不取决于“规范”二字,而取决于它干啥、什么时候必须跑、会不会卡住用户看到内容——defer 和 async 的存在,已经让“必须放 </body> 前”这个老结论失效了。
需要操作 DOM 的主业务脚本该放哪儿
绝大多数应用入口脚本(比如 app.js、main.mjs)应直接用 type="module" 或显式加 defer,然后放在 <head> 里。这不是妥协,而是现代做法:
-
type="module"脚本默认带defer行为,下载不阻塞解析,执行在 DOM 构建完成后、DOMContentLoaded前,DOM 元素全可用 - 不用等
DOMContentLoaded事件监听器,也不用把脚本拖到</body>底部再“碰运气” - 避免 FOUC(未样式化内容闪烁):脚本在
<head>就开始下载,但不执行,DOM 渲染不受影响,样式加载完后脚本立刻接管 - 内联脚本不能加
defer,所以如果非得写内联逻辑且要操作 DOM,唯一安全时机是放在</body>前——但这种情况越来越少
async 脚本只适合完全独立的场景
async 不是“快一点的 defer”,它是“谁先下完谁先跑”,顺序不可控,DOM 可能还没建好:
- 典型适用:统计代码(
analytics.js)、错误监控(error-tracker.min.js)、广告 SDK —— 它们不读取或修改页面结构,也不依赖其他脚本 - 绝对不要给
utils.js或router.js加async,否则可能报ReferenceError: $ is not defined或document.getElementById(...) is null -
async脚本即使放在<head>,也可能在<body>解析一半时执行,此时document.body存在,但你想找的<div id="app">还没出现
Polyfill 和第三方初始化脚本必须早下载、晚执行
像 polyfill.io、webfontloader、某些 CDN 版 UI 组件库(如 bootstrap.bundle.min.js),它们得尽早发起请求,但又不能打断首屏渲染:
使用 PptxGenJS 和 Python 解析,将结构化的 HTML/CSS 幻灯片内容转换为原生、可编辑的 PowerPoint(.pptx)演示文稿。
立即学习“前端免费学习笔记(深入)”;
- 唯一合规解法:放在
<head>,且必须带defer - 别用
async—— Polyfill 需要按顺序执行,比如 Promise polyfill 必须在 async/await 语法使用前就位 - 别放
</body>前 —— 网络延迟可能导致关键 API(如fetch、IntersectionObserver)在首屏交互开始后才补上,用户点按钮没反应 - 注意:内联
<script>console.log('hi')</script>加了defer会被忽略,浏览器当普通脚本立即执行,所以defer只对带src的外部脚本生效
内联脚本和 document.write 是历史包袱
现在几乎没有任何正当理由写内联脚本去操作 DOM,除非你在写一个无法构建、纯静态托管的微型页面:
-
document.write在 DOM 解析完成后调用会清空整个页面,且在defer或模块脚本中根本不可用 - 内联脚本放
<head>里大概率报document.getElementById(...) is null;放</body>前看似安全,但破坏可维护性,也难做代码分割和 tree-shaking - 构建工具(Vite/Webpack)打包后的产物,默认就是
type="module"+defer行为,手动挪位置反而容易出错
真正容易被忽略的不是“放哪”,而是混淆 defer 和 async 的语义边界——前者保序、后者抢跑;还有人以为加了 defer 就能放心往 <head> 塞任何脚本,却忘了内联脚本不响应这个属性。脚本加载策略的本质,是控制执行时机与依赖关系,不是调整 HTML 标签物理位置。


















