根本原因是async和defer仅对带src的外部脚本生效,混用、用于内联脚本或与动态插入/模块脚本共存时会失效,且不解决执行阶段的主线程阻塞问题。

为什么加了async或defer还是白屏卡住
根本原因不是属性没生效,而是你混用了async和defer,或者把它们加在了内联脚本上——浏览器直接忽略,脚本照常阻塞。
-
async和defer只对带src的外部脚本有效;<script>init();</script>加了defer也毫无作用 - 同一页面里同时存在
<script src="a.js" defer></script>和<script src="b.js" async></script>,b.js可能在a.js还没开始下载时就执行完,导致a.js依赖的变量未定义 - 第三方脚本如
umami.js或plausible.js确实适合async,但如果你在它后面紧跟着写<script>umami.track();</script>(内联),这个内联脚本会同步执行,且无法访问还没加载完的 umami 对象
defer 脚本执行时报 document.body 为 null 怎么办
这不是 defer 失效,而是你把脚本放到了 <head> 里却没意识到:defer 保证的是“DOM 解析完成后再执行”,但不保证所有 DOM 元素都已挂载到 document.body —— 尤其当脚本位于 <head> 且 HTML 主体极短时,body 可能尚未被解析出来。
- 确认脚本是否真需要操作
body:如果只是初始化 UI 组件(如轮播图、弹窗),应确保目标容器元素(如<div id="carousel">)已在 HTML 中声明,且位置靠前 - 避免在
defer脚本开头就写document.body.appendChild(...);改用document.getElementById('app')或document.querySelector('#main')显式定位已有节点 - SSR 页面中,服务端注入的
window.__INITIAL_STATE__必须出现在所有defer脚本之前,且不能加defer,否则 JS 执行时该变量还未就位
多个 defer 脚本仍执行错乱的常见原因
defer 本身保序,但保序的前提是“全部 defer、全部外链、全部出现在同一份 HTML 中”。现实里容易踩坑的点很具体。
- Webpack/Vite 打包后生成多个
chunk,比如vendor.js和main.js,若只给main.js加defer,而vendor.js没加,后者会同步执行并可能提前污染全局环境 - 动态插入的脚本(如通过
document.createElement('script')添加)不受defer控制,它的执行时机由 JS 引擎决定,与 HTML 中的顺序无关 -
type="module"脚本默认等效于defer,但不支持async;如果混用<script type="module" src="a.mjs"></script>和<script defer src="b.js"></script>,模块脚本会先执行(因模块有静态导入分析机制),顺序实际失控
async 脚本里 document.getElementById 返回 null 怎么兜底
async 脚本执行时机不可控,不能假设 DOM 已就绪。硬加 setTimeout 或轮询是下策,真正轻量又可靠的判断方式很有限。
立即学习“前端免费学习笔记(深入)”;
- 优先检查
document.readyState:值为'interactive'或'complete'时才安全操作 DOM;'loading'表示还在解析中,应延迟 - 简单场景可用
if (document.body) { init(); },但注意:某些框架(如 Vue SSR)首次渲染时body存在,但目标挂载点(如#app)可能还没解析到——得查具体元素是否存在 - 不要在 async 脚本里调
document.write():它会清空整个文档,且async和defer都禁用此行为
main.js 即使加了 defer,也可能在 DOM 就绪后瞬间占用主线程,造成卡顿。这时候得配合 requestIdleCallback 或代码分割,而不是再加一个属性。



















