在现代 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 动态创建
- 执行顺序:不保证,取决于下载完成时间。
- 适用场景:
- 需要根据条件(如用户行为、设备类型)动态加载脚本。
- 实现模块化加载或懒加载。
- 优点:灵活性高,可以精确控制加载时机。
- 缺点:需要编写额外的 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),它提供了
define
和require
函数来管理模块的定义和依赖。 - 实现 AMD (Asynchronous Module Definition) 或 CMD 规范。
- 引入一个模块加载器库(如 RequireJS),它提供了
- 适用场景:
- 大型传统项目,需要复杂的模块管理和依赖注入。
- 优点:功能强大,支持复杂的依赖管理。
- 缺点:需要引入额外库,现代项目更多使用 Webpack/Rollup 等打包工具 + 动态
import()
。
核心区别对比表
特性 | async | defer | 动态创建 <script> | 动态 import() |
---|---|---|---|---|
阻塞 HTML 解析 | ❌ 否 | ❌ 否 | ❌ 否 | ❌ 否 |
执行时机 | 下载完立即执行 | HTML 解析完成后,DOMContentLoaded 前 | 下载完立即执行 | 加载成功后执行 (Promise ) |
执行顺序 | ❌ 不保证 | ✅ 保证 (按 HTML 顺序) | ❌ 不保证 | ✅ 可通过 Promise 控制 |
依赖 DOM | 可能不安全 | ✅ 安全 (DOM 已就绪) | 取决于插入时机 | ✅ 安全 (通常在事件后调用) |
是否需要额外代码 | ✅ 仅 HTML | ✅ 仅 HTML | ❌ 需要 JS | ❌ 需要 JS |
适用场景 | 独立脚本 (如统计) | DOM 操作脚本,有依赖 | 条件加载,懒加载 | ES6 模块,代码分割 |
总结与最佳实践
- 优先使用
defer
:对于大多数需要操作 DOM 或有依赖关系的脚本,defer
是最佳选择。它不阻塞解析,保证顺序,且在 DOM 就绪后执行。 - 使用
async
:对于完全独立、无依赖、不关心执行顺序的脚本(如第三方统计、广告),使用async
可以让它们尽快执行。 - 避免同步脚本:除非必要,不要使用没有
async
或defer
的<script>
标签,因为它会阻塞页面渲染。 - 动态加载:对于按需加载、懒加载或条件加载,使用动态创建
script
标签或动态import()
。 - 现代应用:在基于模块的现代应用中,优先使用 动态
import()
进行代码分割和懒加载。
通过合理选择这些异步加载方式,可以显著提升网页的加载速度和用户体验。
THE END