脚本加载顺序与异步加载策略详解

HTMLPAGE 团队
15分钟 分钟阅读

深入理解 JavaScript 脚本的加载、解析和执行机制,掌握 async、defer 等异步加载策略,优化页面加载性能。

#性能优化 #JavaScript #异步加载 #defer #async

脚本加载的基本机制

当浏览器遇到 <script> 标签时,默认行为是:暂停 HTML 解析 → 下载脚本 → 执行脚本 → 继续解析。这就是为什么把脚本放在 </body> 前曾是最佳实践。

加载方式阻塞解析执行时机执行顺序
普通下载后立即按出现顺序
async下载后立即不确定
deferDOM 解析完成后按出现顺序
动态插入下载后立即不确定

async vs defer

defer 属性

defer 脚本会在 HTML 解析完成后、DOMContentLoaded 事件前按顺序执行:

<!-- 推荐用于有依赖关系的脚本 -->
<script defer src="/js/vendor.js"></script>
<script defer src="/js/app.js"></script>  <!-- 依赖 vendor.js -->

关键说明: defer 保证执行顺序,所以当 app.js 依赖 vendor.js 时,使用 defer 是安全的。

async 属性

async 脚本下载完成后立即执行,不保证顺序:

<!-- 适用于独立脚本,如统计代码 -->
<script async src="https://www.googletagmanager.com/gtag/js"></script>

选择建议

// 使用 defer 的场景
// - 需要操作 DOM
// - 脚本之间有依赖关系
// - 主应用代码

// 使用 async 的场景
// - 第三方统计、广告脚本
// - 完全独立的功能模块
// - 对执行时机不敏感的脚本

动态脚本加载

基础实现

function loadScript(src: string): Promise<void> {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script')
    script.src = src
    script.async = true
    
    script.onload = () => resolve()
    script.onerror = () => reject(new Error(`Failed to load: ${src}`))
    
    document.head.appendChild(script)
  })
}

// 按需加载
button.addEventListener('click', async () => {
  await loadScript('/js/heavy-feature.js')
  heavyFeature.init()
})

带依赖的加载

async function loadWithDependencies(scripts: string[]) {
  for (const src of scripts) {
    await loadScript(src)  // 顺序加载,保证依赖
  }
}

// 使用
await loadWithDependencies([
  '/js/jquery.min.js',
  '/js/jquery-plugin.js'  // 依赖 jQuery
])

模块化加载

ES Modules

现代浏览器原生支持 ES 模块,自带 defer 行为:

<script type="module" src="/js/app.mjs"></script>

模块脚本的特点:

  • 默认 defer,不阻塞解析
  • 自动严格模式
  • 只执行一次(去重)
  • 支持 import/export

动态导入

// 路由级代码分割
const routes = {
  '/dashboard': () => import('./pages/Dashboard.vue'),
  '/settings': () => import('./pages/Settings.vue')
}

// 按需加载
async function navigate(path: string) {
  const loader = routes[path]
  if (loader) {
    const module = await loader()
    renderComponent(module.default)
  }
}

预加载与预取

preload - 高优先级加载

<!-- 告诉浏览器立即需要此资源 -->
<link rel="preload" href="/js/critical.js" as="script">

prefetch - 低优先级预取

<!-- 告诉浏览器将来可能需要此资源 -->
<link rel="prefetch" href="/js/next-page.js">

modulepreload

<!-- 预加载 ES 模块及其依赖 -->
<link rel="modulepreload" href="/js/app.mjs">

性能优化策略

关键脚本内联

对于首屏关键代码,考虑内联以减少请求:

<script>
  // 关键的首屏逻辑(保持精简)
  document.documentElement.classList.remove('no-js')
</script>

第三方脚本优化

<!-- 延迟加载非关键第三方脚本 -->
<script>
  window.addEventListener('load', () => {
    const script = document.createElement('script')
    script.src = 'https://analytics.example.com/tracker.js'
    document.body.appendChild(script)
  })
</script>

使用 requestIdleCallback

// 在浏览器空闲时加载非关键脚本
if ('requestIdleCallback' in window) {
  requestIdleCallback(() => {
    loadScript('/js/non-critical.js')
  })
} else {
  setTimeout(() => {
    loadScript('/js/non-critical.js')
  }, 2000)
}

最佳实践总结

  1. 首屏关键脚本:内联或使用 preload
  2. 主应用代码:使用 defer 或 type="module"
  3. 独立功能:使用 async
  4. 第三方脚本:延迟加载,在 load 事件后
  5. 大型功能:动态导入,按需加载

正确的脚本加载策略可以显著改善页面加载性能,提升用户体验。