面试题:JavaScript 脚本异步加载如何实现?各有什么区别?

在现代 Web 开发中,异步加载 JavaScript 脚本对于优化页面性能、避免阻塞页面渲染至关重要。以下是几种主要的实现方式及其区别:


1. <script> 标签的 async 属性

  • 语法
<script src="script.js" async></script>
  • 工作原理
    • 浏览器不会阻塞 HTML 解析,脚本在后台下载。
    • 一旦脚本下载完成立即执行
    • 执行时机不确定,可能在 DOMContentLoaded 事件之前或之后。
  • 执行顺序不保证。哪个脚本先下载完就先执行,与它们在 HTML 中的顺序无关。
  • 适用场景
    • 独立的、不依赖其他脚本的脚本(如 Google Analytics、广告脚本)。
    • 不关心执行顺序的脚本。
  • 优点:简单,能显著减少阻塞时间。
  • 缺点:执行顺序不可控,不适合有依赖关系的脚本。

2. <script> 标签的 defer 属性

  • 语法
<script src="script.js" defer></script>
  • 工作原理
    • 浏览器不会阻塞 HTML 解析,脚本在后台下载。
    • 脚本的执行被推迟到整个 HTML 文档解析完成之后,但在 DOMContentLoaded 事件触发之前。
  • 执行顺序保证。按照它们在 HTML 中出现的顺序依次执行。
  • 适用场景
    • 需要在 DOM 构建完成后执行的脚本。
    • 有依赖关系的多个脚本(如先加载库,再加载业务代码)。
  • 优点:不阻塞解析,保证执行顺序,适合 DOM 操作。
  • 缺点:必须等到 HTML 解析完成才执行,对于非 DOM 操作的脚本可能不是最快执行。

3. 动态创建 <script> 标签

  • 语法(JavaScript 代码):
const script = document.createElement('script');
  script.src = 'script.js';
  // async 默认为 true
  document.head.appendChild(script); // 或 document.body.appendChild(script);
  • 工作原理
    • 通过 JavaScript 动态创建 script 元素并插入到 DOM 中。
    • 默认情况下,动态创建的脚本是异步加载和执行的(等同于 async)。
  • 执行顺序不保证,取决于下载完成时间。
  • 适用场景
    • 需要根据条件(如用户行为、设备类型)动态加载脚本。
    • 实现模块化加载或懒加载。
  • 优点:灵活性高,可以精确控制加载时机。
  • 缺点:需要编写额外的 JavaScript 代码。

4. 使用 import 动态导入 (Dynamic import())

  • 语法(ES6+ 模块):
// script.js 必须是 ES6 模块 (export / import)
  import('script.js')
    .then(module => {
      // 脚本加载并执行成功
      module.someFunction();
    })
    .catch(err => {
      // 加载失败
      console.error('Failed to load module', err);
    });
  • 工作原理
    • import() 返回一个 Promise,实现真正的异步模块加载。
    • 脚本在需要时才加载,加载完成后执行并解析模块。
  • 执行顺序:通过 Promise 链可以精确控制。
  • 适用场景
    • 基于 ES6 模块的现代应用。
    • 路由级别的代码分割(如 React、Vue 中的懒加载组件)。
    • 按需加载大型库。
  • 优点:原生支持模块化,返回 Promise,易于处理加载状态和错误。
  • 缺点:需要模块化环境,浏览器兼容性要求较高(现代浏览器支持良好)。

5. 使用第三方库 (如 RequireJS)

  • 工作原理
    • 引入一个模块加载器库(如 RequireJS),它提供了 definerequire 函数来管理模块的定义和依赖。
    • 实现 AMD (Asynchronous Module Definition) 或 CMD 规范。
  • 适用场景
    • 大型传统项目,需要复杂的模块管理和依赖注入。
  • 优点:功能强大,支持复杂的依赖管理。
  • 缺点:需要引入额外库,现代项目更多使用 Webpack/Rollup 等打包工具 + 动态 import()

核心区别对比表

特性asyncdefer动态创建 <script>动态 import()
阻塞 HTML 解析❌ 否❌ 否❌ 否❌ 否
执行时机下载完立即执行HTML 解析完成后,DOMContentLoaded下载完立即执行加载成功后执行 (Promise)
执行顺序❌ 不保证✅ 保证 (按 HTML 顺序)❌ 不保证✅ 可通过 Promise 控制
依赖 DOM可能不安全✅ 安全 (DOM 已就绪)取决于插入时机✅ 安全 (通常在事件后调用)
是否需要额外代码✅ 仅 HTML✅ 仅 HTML❌ 需要 JS❌ 需要 JS
适用场景独立脚本 (如统计)DOM 操作脚本,有依赖条件加载,懒加载ES6 模块,代码分割

总结与最佳实践

  1. 优先使用 defer:对于大多数需要操作 DOM 或有依赖关系的脚本,defer 是最佳选择。它不阻塞解析,保证顺序,且在 DOM 就绪后执行。
  2. 使用 async:对于完全独立、无依赖、不关心执行顺序的脚本(如第三方统计、广告),使用 async 可以让它们尽快执行。
  3. 避免同步脚本:除非必要,不要使用没有 asyncdefer<script> 标签,因为它会阻塞页面渲染。
  4. 动态加载:对于按需加载、懒加载或条件加载,使用动态创建 script 标签或动态 import()
  5. 现代应用:在基于模块的现代应用中,优先使用 动态 import() 进行代码分割和懒加载。

通过合理选择这些异步加载方式,可以显著提升网页的加载速度和用户体验。

THE END
喜欢就支持一下吧
点赞8 分享