页面加载时script标签如何避免阻塞DOM解析

作者:技术拾光时间:2026-08-18 08:53:13浏览:596

在多数场景下,为外部脚本添加 defer 通常是更省心、也更稳妥的做法,但前提是要同时满足几个条件:脚本通过外部文件引入、确实需要操作 DOM,并且执行顺序不能被打乱。如果脚本本身完全独立,例如统计埋点、广告跟踪这类功能,那么使用 async 往往更合适。至于把 放到 前,这种方式只是把阻塞延后到页

在多数场景下,为外部脚本添加 defer 通常是更省心、也更稳妥的做法,但前提是要同时满足几个条件:脚本通过外部文件引入、确实需要操作 DOM,并且执行顺序不能被打乱。如果脚本本身完全独立,例如统计埋点、广告跟踪这类功能,那么使用 async 往往更合适。至于把 放到 前,这种方式只是把阻塞延后到页面底部,并没有真正避免解析阻塞。

页面加载时script标签如何避免阻塞DOM解析

默认 script 为什么会阻塞 DOM 解析

浏览器会以流式方式解析 HTML。当遇到没有 defer 或 async 的 时,会立即暂停 DOM 构建,发起脚本请求,并在下载、解析、执行全部完成后,才继续后续 HTML 的解析。即使脚本内容只有一行 console.log,只要它位于 中, 里的文本、按钮和图片都需要等待它执行完成。

  • 常见表现:在 Network 面板中,DOMContentLoaded 时间明显晚于 first-contentful-paint
  • 在移动端弱网环境下,用户点击按钮可能没有响应,控制台还会出现 document.getElementById(...) is null
  • 当 放在 中间时,后续 HTML 元素不会继续解析,必须等脚本执行结束

defer 属性如何写才能生效

defer 并不是加上就一定有效,它只对满足条件的外部脚本生效:一是必须带有 src 属性,二是 defer 需要以无值布尔属性形式出现。只要写法不符合要求,浏览器通常不会报错,也不会提示失效,因此很容易被忽略。

  • 正确写法:
  • 无效写法:(内联脚本上的 defer 会被忽略)
  • 无效写法:(defer 是布尔属性,不识别赋值形式)
  • 无效写法:(async 与 defer 同时存在时,defer 会被忽略)
  • 构建工具如 Vite 在启用 build.inlineDynamicImports: true 后,可能把外部脚本转成内联形式,导致 defer 完全失效,因此需要检查最终生成的 HTML

async 和 defer 的执行时机区别

async 和 defer 的核心区别在于执行时机。带有 async 的脚本一旦下载完成,就会立即执行,不会等待 DOM 是否构建完成;而带有 defer 的脚本会在 DOM 树构建完成后执行,并且执行时机位于 DOMContentLoaded 事件触发之前。这个差异会直接影响诸如 document.getElementById、getElementsByClassName 等 DOM 操作是否能够安全运行。

  • async 适用于:Google Analytics、百度统计、广告 SDK 等完全独立、不操作 DOM、也不依赖其他脚本的代码
  • defer 适用于:主业务逻辑、UI 组件初始化、需要读取 data- 属性或绑定事件的脚本
  • 多个 async 脚本的执行顺序不固定,而多个 defer 脚本会严格按照 HTML 中的书写顺序执行
  • type="module" 脚本默认具有类似 defer 的行为,因此再额外添加 defer 不会产生作用
  • document.write() 出现在 defer 或 async 脚本中时,会清空整个文档,因此必须移除

动态创建 script 标签的特点

通过 JavaScript 动态创建 标签,并插入到 document.head 或 document.body 中,可以更灵活地控制加载时机,也便于实现错误重试和避免重复加载。不过,这种方式会绕过预加载扫描器(preload scanner),浏览器无法提前发现并并发请求这些资源。

  • 优点:可以先检查 window.myLib 是否存在,再决定是否继续加载;也可以监听 load 和 error 事件实现 fallback
  • 缺点:失去浏览器原生的资源优先级调度能力,构建工具也可能无法静态分析依赖关系
  • 示例:const s = document.createElement('script'); s.src = '/app.js'; s.onload = () => init(); document.head.appendChild(s);
  • 注意:动态插入的 默认不会阻塞渲染;如果需要阻塞渲染,需要使用新属性 blocking="render",目前仅 Chromium 125+ 支持

script 避免阻塞时需要注意的关键点

容易被忽略的一点是:即使使用了 defer,脚本执行仍然会阻塞 DOMContentLoaded 事件的触发;而 async 在执行时依旧会中断 HTML 解析。也就是说,它们并不是完全不阻塞,而是把阻塞发生的时机和位置做了调整。是否需要阻塞、在哪个阶段阻塞,仍然要根据脚本用途和页面体验目标来决定。