JavaScript 脚本加载策略详解
1. 脚本加载策略概述
1.1 为什么需要不同的加载策略
在现代 Web 开发中,JavaScript 脚本的加载策略对页面性能有着至关重要的影响。不同的加载策略可以帮助我们:
- 优化首屏加载速度
- 提升用户体验
- 合理利用网络资源
- 处理脚本间的依赖关系
1.2 对页面性能的影响
- 阻塞渲染:不当的脚本加载会导致页面白屏时间延长
- 资源加载顺序:影响关键资源的加载时机
- 执行时机:影响 DOM 操作和事件绑定的时机
2. 三种主要加载方式详解
2.1 常规加载(阻塞式)
html
<script src="normal-script.js"></script>
特点:
- 立即下载并执行
- 阻塞 HTML 解析
- 保证执行顺序
适用场景:
- 页面核心功能脚本
- 需要立即执行的代码
- 依赖 DOM 结构的脚本
实际项目案例:
javascript
// 电商网站首页核心功能
// 1. 用户登录状态检查
// 2. 购物车数据初始化
// 3. 商品推荐算法初始化
// core.js
document.addEventListener('DOMContentLoaded', () => {
// 1. 检查用户登录状态
const userToken = localStorage.getItem('userToken');
if (userToken) {
initializeUserSession(userToken);
}
// 2. 初始化购物车
const cartData = JSON.parse(localStorage.getItem('cartData') || '{}');
initializeShoppingCart(cartData);
// 3. 初始化商品推荐
const userPreferences = getUserPreferences();
initializeRecommendations(userPreferences);
});
// 在 HTML 中的使用
<script src="/js/core.js"></script>
2.2 异步加载(async)
html
<script async src="async-script.js"></script>
特点:
- 异步下载
- 下载完成后立即执行
- 不保证执行顺序
适用场景:
- 统计代码
- 广告脚本
- 第三方插件
- 独立功能模块
实际项目案例:
javascript
// 新闻网站的文章页面
// 1. 文章阅读时长统计
// 2. 相关文章推荐
// 3. 社交分享功能
// article-analytics.js
async function trackArticleReading() {
const articleId = document.querySelector('[data-article-id]').dataset.articleId;
const startTime = Date.now();
// 监听用户滚动事件
window.addEventListener('scroll', debounce(() => {
const readingTime = Math.floor((Date.now() - startTime) / 1000);
sendAnalyticsData({
articleId,
readingTime,
scrollDepth: calculateScrollDepth()
});
}, 1000));
}
// 在 HTML 中的使用
<script async src="/js/article-analytics.js"></script>
<script async src="/js/social-share.js"></script>
<script async src="/js/related-articles.js"></script>
2.3 延迟加载(defer)
html
<script defer src="defer-script.js"></script>
特点:
- 异步下载
- 等待 HTML 解析完成后执行
- 保证执行顺序
适用场景:
- 非关键功能脚本
- 需要操作 DOM 的脚本
- 有依赖关系的脚本
实际项目案例:
javascript
// 在线教育平台的课程页面
// 1. 课程评论系统
// 2. 学习进度保存
// 3. 笔记功能
// course-features.js
document.addEventListener('DOMContentLoaded', () => {
// 1. 初始化评论系统
initializeComments({
courseId: getCourseId(),
userId: getUserId()
});
// 2. 初始化学习进度跟踪
initializeProgressTracking({
courseId: getCourseId(),
userId: getUserId(),
autoSave: true
});
// 3. 初始化笔记功能
initializeNoteTaking({
courseId: getCourseId(),
userId: getUserId(),
syncInterval: 30000
});
});
// 在 HTML 中的使用
<script defer src="/js/course-features.js"></script>
2.4 动态加载
适用场景:
- 按需加载的功能模块
- 用户交互触发的功能
- 条件性功能
实际项目案例:
javascript
// CRM系统中集成腾讯会议功能
// 1. 动态加载腾讯会议 JSSDK
// 2. 初始化会议功能
// 3. 处理会议相关事件
// tencent-meeting.js
class TencentMeetingLoader {
static async loadSDK() {
if (window.TencentMeeting) {
return window.TencentMeeting;
}
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://meeting.tencent.com/sdk/v1.0.0/meeting.js';
script.async = true;
script.onload = () => {
// SDK 加载完成后的初始化
window.TencentMeeting.init({
sdkAppID: 'your-sdk-app-id',
userID: getCurrentUserId(),
userSig: generateUserSig(),
// 其他配置项...
}).then(() => {
resolve(window.TencentMeeting);
}).catch(reject);
};
script.onerror = reject;
document.head.appendChild(script);
});
}
static async createMeeting(options) {
try {
const meeting = await this.loadSDK();
return await meeting.createMeeting({
topic: options.topic,
startTime: options.startTime,
duration: options.duration,
// 其他会议参数...
});
} catch (error) {
console.error('创建会议失败:', error);
throw error;
}
}
}
// 使用示例
document.getElementById('create-meeting-btn').addEventListener('click', async () => {
try {
const meeting = await TencentMeetingLoader.createMeeting({
topic: '客户需求沟通会议',
startTime: new Date(),
duration: 60
});
// 处理会议创建成功
showMeetingInfo(meeting);
} catch (error) {
// 处理错误
showErrorMessage('创建会议失败,请稍后重试');
}
});
2.5 预加载
适用场景:
- 关键资源预加载
- 用户可能访问的页面
- 大型资源文件
实际项目案例:
javascript
// 商城网站图片懒加载优化
// 1. 预加载可视区域图片
// 2. 预加载下一屏图片
// 3. 预加载用户可能感兴趣的商品图片
// image-preloader.js
class ImagePreloader {
constructor() {
this.observer = null;
this.preloadQueue = new Set();
}
// 初始化懒加载
init() {
// 创建 Intersection Observer
// 使用IntersectionObserver需要注意兼容性
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
// 预加载下一张图片
this.preloadNextImage(entry.target);
}
});
},
{
rootMargin: '50px 0px', // 提前 50px 开始加载
threshold: 0.1
}
);
// 观察所有需要懒加载的图片
document.querySelectorAll('img[data-src]').forEach(img => {
this.observer.observe(img);
});
}
// 加载图片
loadImage(img) {
if (img.dataset.src) {
// 创建预加载链接
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'image';
link.href = img.dataset.src;
document.head.appendChild(link);
// 设置图片源
img.src = img.dataset.src;
img.removeAttribute('data-src');
// 从预加载队列中移除
this.preloadQueue.delete(img);
}
}
// 预加载下一张图片
preloadNextImage(currentImg) {
const nextImg = currentImg.nextElementSibling;
if (nextImg && nextImg.tagName === 'IMG' && nextImg.dataset.src) {
this.preloadQueue.add(nextImg);
this.loadImage(nextImg);
}
}
// 预加载推荐商品图片
preloadRecommendedImages() {
const recommendedProducts = document.querySelectorAll('.recommended-product img[data-src]');
recommendedProducts.forEach(img => {
this.preloadQueue.add(img);
this.loadImage(img);
});
}
}
// 使用示例
document.addEventListener('DOMContentLoaded', () => {
const preloader = new ImagePreloader();
preloader.init();
// 当用户滚动到推荐商品区域时
const recommendedSection = document.querySelector('.recommended-products');
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
preloader.preloadRecommendedImages();
observer.disconnect();
}
});
observer.observe(recommendedSection);
});
// HTML 示例
<img data-src="/images/products/product1.jpg" alt="商品1" class="lazy">
<img data-src="/images/products/product2.jpg" alt="商品2" class="lazy">
<img data-src="/images/products/product3.jpg" alt="商品3" class="lazy">
3. 最佳实践建议
3.1 脚本位置优化
- 关键脚本放在
<head>
中 - 非关键脚本放在
</body>
前 - 使用
defer
或async
属性优化加载
3.2 加载策略选择
- 根据依赖关系选择加载方式
- 根据执行时机选择加载方式
- 根据功能重要性选择加载方式
3.3 性能优化技巧
- 合并压缩脚本
- 使用 CDN
- 预加载关键资源
- 实现代码分割
- 使用 HTTP/2
4. 常见问题解决方案
4.1 脚本加载失败处理
javascript
function loadScriptWithRetry(src, maxRetries = 3) {
return new Promise((resolve, reject) => {
let retries = 0;
function attempt() {
loadScript(src)
.then(resolve)
.catch(error => {
if (retries < maxRetries) {
retries++;
setTimeout(attempt, 1000 * retries);
} else {
reject(error);
}
});
}
attempt();
});
}
4.2 加载状态监控
javascript
// 监控脚本加载状态
function monitorScriptLoading() {
const scripts = document.getElementsByTagName('script');
const loadingStatus = {};
Array.from(scripts).forEach(script => {
const startTime = performance.now();
script.onload = () => {
const loadTime = performance.now() - startTime;
loadingStatus[script.src] = {
status: 'loaded',
loadTime
};
};
script.onerror = () => {
loadingStatus[script.src] = {
status: 'error',
loadTime: performance.now() - startTime
};
};
});
return loadingStatus;
}