没加 async 或 defer 的 analytics.js 会阻塞 DOM 构建,因浏览器流式解析时遇无属性 script 会暂停、下载并执行完才继续,导致首屏白屏。

analytics.js 这类第三方统计脚本,只要没加 async 或 defer,就会直接卡住 DOM 构建——不是它慢,是浏览器被强制暂停解析,等它下载+执行完才继续。首屏白屏几秒,八成是它在 <head> 里干的。
为什么没加 async/defer 的 analytics.js 会阻塞 DOM 构建
浏览器解析 HTML 是流式、自上而下的。遇到 <script src="https://cdn.example.com/analytics.js"></script>(无属性),立刻做三件事:暂停 HTML 解析 → 发起请求下载脚本 → 下载完立即执行 → 才恢复解析。此时 document.body 可能还是 null,但很多统计 SDK 会尝试读取或操作 DOM,要么报错,要么静默失败。
- 同步脚本在
<head>中,DOM 树根本还没开始构建,document.getElementById必然返回null - 即使脚本本身只有 2KB,DNS+TCP+TLS 握手耗时就可能达 400ms,这期间页面完全空白
- 多个未标注的第三方脚本(如
chat-widget.js+ab-test-sdk.js)会串行阻塞,叠加延迟
async 是唯一合理选项,但得避开常见误用
统计脚本本质是“发个 Beacon 就完事”,不依赖 DOM、不提供全局函数、不与其他脚本耦合——这正是 async 的理想场景。但它不是万能解药,用错照样翻车。
-
async对内联脚本无效:<script async>console.log('hi')</script>会被忽略,只对带src的外部脚本生效 - 不能和
defer混用:<script async defer src="analytics.js"></script>中defer被浏览器无视,行为退化为纯async - 跨域脚本必须加
crossorigin:<script async crossorigin src="https://cdn.example.com/analytics.js"></script>,否则错误堆栈丢失,调试时只能看到Script error.
更进一步:交互后加载,比 async 还省资源
连 async 都算“过早加载”——用户打开页面 0.5 秒内就触发统计请求,其实毫无必要。真正该做的,是把加载时机往后推到用户产生真实意图之后。
立即学习“前端免费学习笔记(深入)”;
- 监听首次点击或滚动:
document.addEventListener('click', loadAnalytics, { once: true }) - 用动态插入替代硬编码:
loadScript('https://cdn.example.com/analytics.js', { async: true })(函数需自行实现,内部用document.createElement('script')) - 配合
AbortSignal.timeout(3000)防挂死:避免 CDN 故障导致脚本加载卡住主线程
验证是否真解除了阻塞,别只看有没有报错
写了 async 不代表成功。关键要看 Network 面板里该请求的 Initiator 列是不是 parser —— 如果是,说明浏览器仍在按同步逻辑处理它,大概率是属性拼写错误(比如写成 asyn)、或脚本被其他逻辑动态插入却没设属性。
- 正确值应为
other或空,表示由脚本引擎主动发起,非 HTML 解析器阻塞触发 - 在脚本开头加
console.timeStamp('analytics-start'),对比DOMContentLoaded时间戳,确认执行时间点是否在 DOM 构建完成后(正常应晚于它) - 用 Lighthouse 跑一次,重点看 “Eliminate render-blocking resources” 是否还列出该脚本
最危险的不是脚本本身重,而是你默认信任它的加载方式。一个没加 async 的 umami.js,和一段 10 行内联 console.log,对首屏的影响天壤之别——前者能让白屏从 0.3s 拉到 1.8s,后者几乎不可测。优化不是删代码,是重新决定“谁该什么时候说话”。



















