JS 异步加载设计方案,适合用于网站性能优化、后台系统、CMS 内容站、专题页、广告/统计脚本、组件化页面等场景。

JS异步加载设计详解:defer、async、动态导入与懒加载最佳实践


JS 异步加载设计

一、设计目标

JS 异步加载的核心目标是:

  1. 避免阻塞页面渲染

  2. 提升首屏加载速度

  3. 按需加载非核心功能

  4. 降低主线程压力

  5. 保证脚本执行顺序可控

  6. 兼容统计、广告、第三方 SDK 等外部脚本


二、JS 加载方式分类

1. 普通同步加载

<script src="/js/main.js"></script>

特点:

  • HTML 解析会被阻塞

  • JS 下载和执行完成后,页面才继续解析

  • 适合极少量关键脚本

  • 不适合大量业务 JS

不推荐大量使用。


2. defer 延迟加载

<script src="/js/main.js" defer></script>

特点:

  • 不阻塞 HTML 解析

  • 脚本会在 DOM 解析完成后执行

  • 多个 defer 脚本会按照书写顺序执行

  • 适合大多数业务 JS

推荐用于:

<script src="/js/vendor.js" defer></script>
<script src="/js/main.js" defer></script>

适合页面主体功能、导航、搜索、评论、登录状态等。


3. async 异步加载

<script src="/js/analytics.js" async></script>

特点:

  • 不阻塞 HTML 解析

  • 下载完成后立即执行

  • 多个 async 脚本不保证执行顺序

  • 适合互不依赖的第三方脚本

推荐用于:

<script src="https://www.googletagmanager.com/gtag/js?id=xxx" async></script>

适合统计、广告、埋点、独立 SDK。


三、推荐加载策略

1. 核心业务 JS 使用 defer

<script src="/assets/js/runtime.js" defer></script>
<script src="/assets/js/vendor.js" defer></script>
<script src="/assets/js/app.js" defer></script>

适合:

  • 页面交互逻辑

  • 用户登录状态

  • 搜索框

  • 导航菜单

  • 内容详情页功能

  • 后台管理系统页面逻辑

优点是执行顺序稳定,不阻塞 HTML 解析。


2. 第三方脚本使用 async

<script src="https://example.com/sdk.js" async></script>

适合:

  • 统计代码

  • 广告代码

  • 分享组件

  • 客服工具

  • 第三方推荐系统

  • A/B 测试工具

但要注意: 如果第三方脚本依赖某个全局变量,不能直接无脑 async


3. 非首屏功能按需加载

例如用户点击按钮后再加载:

function loadScript(src) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script')
    script.src = src
    script.async = true
    script.onload = resolve
    script.onerror = reject
    document.head.appendChild(script)
  })
}

document.querySelector('#open-comment').addEventListener('click', async () => {
  await loadScript('/js/comment.js')
  window.Comment.init()
})

适合:

  • 评论系统

  • 分享弹窗

  • 富文本编辑器

  • 图表组件

  • 地图组件

  • 视频播放器

  • AI 对话组件

  • 后台复杂表单组件


四、模块化异步加载设计

如果使用现代前端框架,可以使用动态导入:

const module = await import('./chart.js')
module.renderChart()

或者:

button.addEventListener('click', async () => {
  const { openDialog } = await import('./dialog.js')
  openDialog()
})

适合:

  • Vue / React / Next.js / Nuxt

  • Vite / Webpack

  • 后台管理系统

  • 大型内容平台

优点:

  • 自动代码分割

  • 减少首屏 JS 体积

  • 按页面或功能加载


五、按页面类型设计加载策略

1. 首页

首页通常追求首屏速度。

建议:

<script src="/js/home.js" defer></script>
<script src="/js/analytics.js" async></script>

加载策略:

脚本类型加载方式
首页主交互defer
统计代码async
广告脚本async / 懒加载
推荐模块滚动到可视区后加载
弹窗组件用户触发后加载

2. 文章详情页

文章页重点是内容优先展示。

建议:

<script src="/js/article.js" defer></script>

评论、分享、相关推荐可以延后加载:

if ('IntersectionObserver' in window) {
  const observer = new IntersectionObserver(async entries => {
    if (entries[0].isIntersecting) {
      await import('./related.js')
      observer.disconnect()
    }
  })

  observer.observe(document.querySelector('#related'))
}

适合:

  • 相关推荐

  • 评论区

  • 点赞收藏

  • 分享浮层

  • 阅读进度条

  • 图片懒加载增强逻辑


3. 后台管理系统

后台系统 JS 较重,建议分模块异步加载:

const routes = [
  {
    path: '/article/edit',
    component: () => import('./pages/ArticleEdit.vue')
  },
  {
    path: '/category/list',
    component: () => import('./pages/CategoryList.vue')
  }
]

设计原则:

模块加载策略
登录页单独打包
首页 Dashboard单独打包
富文本编辑器按需加载
图表组件按需加载
文件上传按需加载
权限管理路由级加载
公共组件合理拆包

六、第三方脚本异步加载设计

第三方脚本建议统一封装加载器。

const ScriptLoader = {
  cache: new Map(),

  load(src) {
    if (this.cache.has(src)) {
      return this.cache.get(src)
    }

    const promise = new Promise((resolve, reject) => {
      const script = document.createElement('script')
      script.src = src
      script.async = true
      script.onload = () => resolve(script)
      script.onerror = () => reject(new Error(`Script load failed: ${src}`))
      document.head.appendChild(script)
    })

    this.cache.set(src, promise)
    return promise
  }
}

使用:

await ScriptLoader.load('https://example.com/sdk.js')
window.ExampleSDK.init()

这样可以避免重复加载。


七、执行顺序设计

有依赖关系时,不要使用多个 async

错误示例:

<script src="/js/jquery.js" async></script>
<script src="/js/plugin.js" async></script>

因为 plugin.js 可能先执行,导致找不到 jQuery。

正确方式:

<script src="/js/jquery.js" defer></script>
<script src="/js/plugin.js" defer></script>

或者动态链式加载:

await loadScript('/js/jquery.js')
await loadScript('/js/plugin.js')

八、首屏性能推荐方案

建议把 JS 分为四类:

类型示例加载方式
核心渲染脚本页面基础交互defer
首屏必要脚本导航、登录状态defer
非首屏脚本评论、推荐、图表懒加载
第三方脚本统计、广告、客服async

推荐结构:

<head>
  <link rel="preload" href="/js/app.js" as="script">
  <script src="/js/app.js" defer></script>
</head>
<body>
  <main>
    页面内容
  </main>

  <script>
    window.addEventListener('load', function () {
      import('/js/non-critical.js')
    })
  </script>
</body>

九、懒加载触发方式

1. 页面加载完成后加载

window.addEventListener('load', () => {
  import('./analytics.js')
})

适合非关键逻辑。


2. 用户交互后加载

document.querySelector('#share').addEventListener('click', async () => {
  const share = await import('./share.js')
  share.open()
})

适合分享、弹窗、评论、登录框。


3. 滚动到可视区域后加载

const target = document.querySelector('#comment')

const observer = new IntersectionObserver(async entries => {
  if (entries[0].isIntersecting) {
    const comment = await import('./comment.js')
    comment.init()
    observer.disconnect()
  }
})

observer.observe(target)

适合评论区、相关推荐、广告位。


4. 浏览器空闲时加载

if ('requestIdleCallback' in window) {
  requestIdleCallback(() => {
    import('./recommend.js')
  })
} else {
  setTimeout(() => {
    import('./recommend.js')
  }, 2000)
}

适合低优先级模块。


十、异常处理设计

异步加载必须考虑失败情况:

async function loadComment() {
  try {
    const comment = await import('./comment.js')
    comment.init()
  } catch (error) {
    console.error('评论模块加载失败', error)
    document.querySelector('#comment').innerHTML = '评论加载失败,请稍后重试'
  }
}

推荐增加:

  • 加载中状态

  • 加载失败提示

  • 重试机制

  • 超时机制

  • 降级方案


十一、完整推荐方案

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>JS 异步加载示例</title>

  <!-- 核心业务 JS -->
  <script src="/js/vendor.js" defer></script>
  <script src="/js/app.js" defer></script>

  <!-- 第三方统计 -->
  <script src="https://example.com/analytics.js" async></script>
</head>
<body>

  <header id="header"></header>
  <main id="app"></main>
  <section id="comment"></section>

  <script>
    const commentEl = document.querySelector('#comment')

    const observer = new IntersectionObserver(async entries => {
      if (entries[0].isIntersecting) {
        try {
          const comment = await import('/js/comment.js')
          comment.init()
        } catch (e) {
          commentEl.innerHTML = '评论模块加载失败'
        }

        observer.disconnect()
      }
    })

    observer.observe(commentEl)
  </script>

</body>
</html>

十二、最佳实践总结

JS异步加载设计详解:defer、async、动态导入与懒加载最佳实践

场景推荐方式
页面主业务 JSdefer
独立第三方 JSasync
有依赖关系的 JSdefer 或顺序加载
大型组件import() 动态导入
非首屏模块IntersectionObserver
用户触发功能点击后加载
低优先级功能requestIdleCallback
后台系统页面路由级代码分割
广告/统计async + 容错
富文本/图表/地图按需加载

十三、推荐落地原则

一句话总结:

核心功能 defer,第三方 async,复杂模块 import,非首屏懒加载。

对于内容型网站,可以这样设计:

首屏内容优先展示
核心交互 defer 加载
统计广告 async 加载
评论推荐滚动加载
复杂组件点击后加载

对于后台管理系统,可以这样设计:

基础框架先加载
页面路由按需加载
大型组件独立拆包
公共依赖合理缓存
第三方 SDK 统一加载器管理