关键JS不能放<head>里直接执行,因为会阻塞HTML解析,导致DOM渲染延迟;应使用defer/async、DOMContentLoaded事件、动态import()等方式解耦执行时机与HTML位置。
<head> 里直接执行因为浏览器解析到 <script> 就会暂停 HTML 构建,等 JS 下载、解析、执行完才继续。哪怕这段 JS 只是初始化一个按钮点击事件,它也会卡住整个 DOM 树生成——首屏元素明明写在 HTML 里,却迟迟不渲染。
常见错误现象:document.getElementById('main') === null 却还在 <head> 里调用;DevTools 的 Performance 面板显示 Parse HTML 长时间被 Script Evaluation 中断。
async/defer)永远阻塞解析,不管体积多小<head> 中的内联脚本比外链更危险:没有下载延迟,但执行更快、阻塞更早defer 和 async 到底该选哪个defer 保证执行顺序,且只在 DOMContentLoaded 前执行;async 下载完立刻执行,不保证顺序,可能在 DOM 还没解析完时就跑。
使用场景很明确:
立即学习“前端免费学习笔记(深入)”;
initApp()、renderHeader())→ 必须用 defer
async
defer 脚本按 HTML 中出现顺序执行;async 脚本谁先下完谁先跑,顺序不可控type="module" 脚本默认行为等同 defer,不用额外加解耦不是去掉 JS,而是切断执行时机与 HTML 位置的强绑定。核心是把“等 DOM 就绪”这件事从开发者手动判断,变成由浏览器自动调度。
DOMContentLoaded 事件包裹逻辑,而不是靠脚本放 <body> 底部“碰运气”onclick="doSomething()",改用 addEventListener 动态绑定(哪怕只绑一次)<script type="application/json"> 内联数据,而非拼字符串到 JS 里data- 属性标记,而不是依赖 id 或 class 名字——名字一改,JS 就失效真正解耦发生在打包时:把关键路径 JS 拆成「主入口」和「功能模块」,用 import() 按需触发,而不是一股脑全加载。
例如首屏只需要渲染列表,就不该把编辑弹窗、导出 Excel 的逻辑打包进 main.js:
const Page = await import('./pages/home.js')
const Modal = await import('./components/ExportModal.js')
import() 返回 Promise,必须处理 loading 状态,否则白屏import() 转成独立 chunk,配合 preload 提前拉取关键模块最易被忽略的一点:解耦不是为了写得爽,而是为了让“关键 JS”真正变小——如果 main.js 仍包含所有业务逻辑,只是换个方式加载,那只是换汤不换药。