内联脚本不会被预解析器扫描,async和defer属性对其无效,因其无src属性、不触发网络请求,仅由主html解析器同步执行;真正异步需外链脚本+async/defer。

内联脚本根本不会被预解析器扫描到
预解析器(preload scanner)只扫描初始 HTML 字节流中静态、非内联、带 src 的 <script></script> 标签。所有内联脚本——无论是否加了 async 或 defer——在预解析阶段直接被跳过。
这意味着:<script async>console.log('a')</script> 中的代码不会被提前下载(本来也没啥可下载),也不会被预解析器“发现”或干预加载时机;它完全依赖主 HTML 解析器走到该位置时才执行,和默认同步脚本一样阻塞解析。
-
<script>init()</script>:立即阻塞,无预扫描,无异步行为 -
<script async>fetch('/api')</script>:仍同步执行,async属性对内联脚本无效,浏览器静默忽略 -
<script defer>doStuff()</script>:同上,defer也无效,文档里写得再漂亮也没用
为什么 async 和 defer 对内联脚本没意义
这两个属性的设计目标是控制「外部资源获取」与「执行时机」的解耦,而内联脚本没有网络请求阶段——它已经随 HTML 一起到达内存。所以浏览器根本不走 Fetch 阶段,自然也谈不上“异步下载”或“延迟执行”。
规范明确要求:async 和 defer 仅对具有 src 属性的 <script></script> 生效。没有 src,就是普通同步执行块。
- 构建工具(如 Vite 的
build.rollupOptions.output.inlineDynamicImports: true)可能把模块转成内联代码,此时你加的defer就彻底失效 - 想让逻辑“不阻塞”,必须拆出去:
<script src="logic.js" async></script>才是真正异步 - 内联脚本若含
document.write(),还会触发整个页面重置,这种写法应从项目中清除
预解析器能看见的只有外链脚本,且有严格条件
预解析器不是 JS 引擎,它不执行、不解析语法、不关心内容,只做一件事:在 HTML 解析器被阻塞时,快速向前扫描字节流,找可发起网络请求的资源声明。
它能识别并提前下载的只有这些:
-
<script src="a.js"></script>(同步外链,会被预扫描并下载) -
<script src="b.js" async></script>(会被发现并提前下载,但执行不可控) -
<script src="c.js" defer></script>(同样被发现下载,执行顺序受保障) -
<link rel="preload" as="script" href="d.js">(手动强干预,绕过解析顺序)
以下全部逃逸预解析:
document.createElement('script')import('./e.js')-
<script type="module">import './f.js'</script>(模块图由 ES 加载器接管) -
<script src="g.js" async></script>被包裹在vite-plugin-html模板逻辑里动态注入——只要不是服务端直出的原始字节流,就不算“初始 HTML”
真正影响首屏的关键点:别在 放内联业务逻辑
很多人以为把小段初始化代码写成内联就能“快一点”,结果反而卡住 HTML 解析器,导致 DOM 构建延迟,进而拖慢 FCP 和 LCP。尤其当这段内联脚本还调用了 document.querySelector 或修改了样式,问题更隐蔽。
更稳妥的做法是:
- 极小 polyfill(如 Promise 检测)可内联,但必须放在
最前,且不含任何 DOM 操作 - 所有业务逻辑、框架初始化、数据拉取,一律外链 +
defer - 如果必须动态加载,用
link[rel=modulepreload]配合import(),而不是靠内联脚本去createElement
预解析机制的底层约束很硬:它只认字节流里的静态标签。任何 JS 控制的加载路径,都等于主动放弃浏览器原生优化能力——这点容易被忽略,但后果往往出现在 LCP 崩溃的监控告警里。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











