面试题:JavaScript 脚本延迟加载的方式有哪些?

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. 使用 setTimeoutrequestIdleCallback

  • 原理:利用定时器或浏览器空闲时间回调,将脚本加载任务推迟到稍后执行。
  • 延迟点执行时机延迟
  • 语法
// 使用 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
喜欢就支持一下吧
点赞6 分享