先记住这个答案
普通外部经典脚本若不加属性,浏览器遇到它时要暂停 HTML 解析,下载并执行后再继续。加 defer 可以边解析边下载,等文档解析完后按标签出现顺序执行,执行完才触发 DOMContentLoaded。加 async 也能边解析边下载,但下载就绪便尽快执行,当时若仍在解析会暂停解析;多个异步脚本没有相互执行顺序保证。依赖 DOM 或其他脚本时选 defer,独立的统计脚本更适合 async。
defer在解析完成后按标签顺序执行async下载完成就执行,不保证顺序defer会延后DOMContentLoaded- 内联经典脚本上的
defer不起作用
两个脚本有依赖该怎么放
页面有一个基础库和一个用它初始化导航的脚本。第二个必须等第一个执行过,且要能找到正文里的导航节点。把两个外部经典脚本都标记为 defer,并按依赖顺序写入 HTML,就能让下载并行、执行有序。
<head>
<script defer src='/js/vendor.js'></script>
<script defer src='/js/nav.js'></script>
</head>
<body>
<nav id='site-nav'>站点导航</nav>
</body>vendor.js 会先于 nav.js 执行,即使后者下载得更快。两者都在文档解析完成后运行,因此 nav.js 可以查询后面的导航节点。若把两条都改成 async,加载速度决定谁先执行,基础库可能还没有准备好,页面便偶发报错。
async 为什么适合独立脚本
广告统计或独立监控脚本通常不需要访问其他脚本创建的全局对象,也不应阻止页面主体尽早解析。给这类外部脚本加 async,网络下载与文档解析并行;脚本一旦准备好就执行。代价是执行时机不固定,不能假设页面节点一定已经全部存在,也不能假设它在另一条 async 脚本之后执行。
async 的含义不是让 JavaScript 在后台线程运行。下载可以并行,但执行脚本仍会占用主线程,执行期间页面处理会受影响。如果代码依赖完整 DOM,应自己检查节点是否可用或改选更适合的加载方式。异步脚本也不参与 DOMContentLoaded 的等待:事件可能先于它执行,别把这个事件当成异步脚本已经完成的证明。
内联脚本和模块脚本的边界
defer 针对带 src 的外部经典脚本。内联经典脚本没有要并行下载的文件,写上 defer 不会把它移到解析结束后执行。模块脚本使用 type='module',默认已经延后到解析之后执行,额外写 defer 没作用;模块脚本若加 async,则按异步模块脚本的时机执行,不能沿用默认的有序等待假设。
讨论脚本性能时,先确认是经典脚本、内联脚本还是模块脚本。同样一个 <script> 标签,因为类型和属性不同,加载和执行顺序会变。项目若由打包工具注入脚本,也要检查最终生成的 HTML,而不是只凭源码中的导入语句猜浏览器时序。
容易答错的地方
- 以为 defer 和 async 都只在页面加载完后执行
defer等文档解析完成;async只等自身下载就绪,可能在解析中途执行。两者的下载都可与解析并行,但执行时间不同。- 以为 async 能保证标签顺序
async不保证多个脚本的相对执行顺序。需要基础库先执行、业务脚本后执行时,使用defer并按顺序放标签,或通过模块依赖明确关系。
面试官还会怎么问?
defer 脚本和 DOMContentLoaded 谁先?
外部经典 defer 脚本在事件之前执行;浏览器会等它们下载并完成求值,再触发 DOMContentLoaded。所以它适合依赖已解析 DOM 的初始化代码。
async 脚本会延迟 DOMContentLoaded 吗?
不会因为自身是 async 就让这个事件等待它。事件可能在它执行前触发;脚本若要依赖该事件,应检查文档当前状态,避免监听器注册得太晚。
内联脚本写 defer 有用吗?
对没有 src 的经典内联脚本没有效果。它仍在解析器遇到的位置执行;需要延后时可改外部脚本配 defer,或选择合适的模块与事件初始化方式。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。