Skip to content

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>
  • 使用 deferasync 属性优化加载

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;
}

参考文档

Released under the MIT License.