JavaScript 脚本的延迟加载(Lazy Loading)指的是将脚本的加载或执行推迟到特定时机,以优化页面初始加载性能,避免阻塞关键渲染路径。它与异步加载(async
)有重叠,但更侧重于“推迟”这一行为。
以下是实现 JavaScript 脚本延迟加载的主要方式:
1. <script>
标签的 defer
属性(推荐基础方式)
- 原理:脚本在后台异步下载,但执行被推迟到整个 HTML 文档解析完成之后,
DOMContentLoaded
事件触发之前。 - 延迟点:执行延迟。
- 语法:
<script src="deferred-script.js" defer></script>
- 优点:
- 不阻塞 HTML 解析。
- 保证按 HTML 中的顺序执行。
- 执行时 DOM 已完全构建,可安全操作 DOM。
- 适用场景:需要操作 DOM 的脚本、有依赖关系的脚本。
2. 动态创建 <script>
标签(经典手动延迟)
- 原理:通过 JavaScript 在需要时(如页面加载后、用户交互后)动态创建
script
元素并插入 DOM,从而实现延迟加载。 - 延迟点:加载和执行都延迟。
- 语法:
// 页面加载完成后延迟加载
window.addEventListener('load', function() {
const script = document.createElement('script');
script.src = 'lazy-script.js';
document.head.appendChild(script);
});
// 或在用户点击按钮时加载
button.addEventListener('click', function() {
const script = document.createElement('script');
script.src = 'feature-script.js';
document.head.appendChild(script);
});
- 优点:灵活性极高,可以精确控制加载时机(基于事件、滚动、视口等)。
- 缺点:需要编写额外的控制代码。
3. 动态 import()
(ES6+ 模块动态导入)
- 原理:使用
import('module-path')
语法,它返回一个Promise
,实现按需加载 ES6 模块。 - 延迟点:加载和执行都延迟,且支持代码分割。
- 语法:
// 延迟加载一个模块
async function loadFeature() {
try {
const module = await import('./feature-module.js');
module.init();
} catch (err) {
console.error('加载失败', err);
}
}
// 在需要时调用
someButton.addEventListener('click', loadFeature);
- 优点:
- 原生支持,语法简洁。
- 返回
Promise
,易于处理成功/失败。 - 与现代打包工具(Webpack, Vite)结合,实现路由级或组件级的代码分割。
- 适用场景:现代前端框架(React, Vue, Angular)中的懒加载组件、按需加载功能模块。
4. Intersection Observer API + 动态加载
- 原理:监听元素是否进入视口(viewport),当某个元素(如图片、视频、或功能区域)即将可见时,再加载对应的脚本。
- 延迟点:基于用户行为/视口的延迟加载。
- 语法:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 元素进入视口,加载脚本
loadScript('below-the-fold-feature.js');
observer.unobserve(entry.target); // 只加载一次
}
});
});
// 监听某个占位元素
const placeholder = document.getElementById('feature-placeholder');
observer.observe(placeholder);
function loadScript(src) {
const script = document.createElement('script');
script.src = src;
document.head.appendChild(script);
}
- 优点:极致优化,只在用户真正需要时才加载资源。
- 适用场景:长页面中“below-the-fold”(折叠内容以下)的功能脚本、评论区脚本、视频播放器脚本。
5. 使用 setTimeout
或 requestIdleCallback
- 原理:利用定时器或浏览器空闲时间回调,将脚本加载任务推迟到稍后执行。
- 延迟点:执行时机延迟。
- 语法:
// 使用 setTimeout 延迟 3 秒加载
setTimeout(() => {
loadScript('non-critical.js');
}, 3000);
// 使用 requestIdleCallback 在浏览器空闲时加载
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
loadScript('low-priority.js');
});
} else {
// 降级处理
window.addEventListener('load', () => loadScript('low-priority.js'));
}
- 优点:确保关键内容优先加载。
- 缺点:
setTimeout
时间难以精确控制;requestIdleCallback
兼容性需考虑。
6. 将 <script>
放在 </body>
之前
- 原理:HTML 解析是自上而下的。将
script
标签放在</body>
之前,可以确保它在 DOM 构建完成后才开始下载和执行。 - 延迟点:执行延迟(因为必须等前面的 DOM 解析完)。
- 语法:
<body>
<!-- 页面内容 -->
<script src="script.js"></script>
</body>
- 注意:这本质上是同步阻塞加载,只是延迟了阻塞的时机。它会阻塞后续的
</body>
和</html>
的解析。现代开发中,应优先使用defer
,效果类似但更优(因为defer
脚本可以并行下载)。
总结
方式 | 核心机制 | 适用场景 |
---|---|---|
defer | 执行推迟到 DOM 解析后 | 通用,需操作 DOM 的脚本 |
动态创建 script | 手动控制加载时机 | 条件加载、事件触发加载 |
动态 import() | ES6 模块按需加载 | 现代应用,代码分割,懒加载组件 |
Intersection Observer | 基于视口可见性加载 | 长页面功能、below-the-fold 内容 |
setTimeout / requestIdleCallback | 基于时间或空闲状态加载 | 低优先级、非关键脚本 |
放在 </body> 前 | 利用 HTML 解析顺序 | 传统做法,建议用 defer 替代 |
最佳实践:
- 关键脚本:使用
defer
。 - 非关键/功能脚本:使用动态
import()
或动态创建script
标签,结合用户交互或视口监听。 - 现代项目:优先采用 动态
import()
实现代码分割和懒加载。
THE END