Vue.js asynchronous component

What is an asynchronous component?

Asynchronous components allow us to split our application into smaller chunks of code and load them from the server only when needed. This can significantly reduce initial load times and improve application performance.

Basic usage

usedefineAsyncComponentDefine an asynchronous component:

<script setup>
import { defineAsyncComponent } from 'vue'

// 异步加载组件
const AsyncComponent = defineAsyncComponent(() =>
  import('./components/HeavyComponent.vue')
)
</script>

<template>
  <div>
    <AsyncComponent />
  </div>
</template>

Loading status and error handling

Loading status, error handling and timeouts can be configured:

<script setup>
import { defineAsyncComponent } from 'vue'
import LoadingComponent from './LoadingComponent.vue'
import ErrorComponent from './ErrorComponent.vue'

const AsyncComponent = defineAsyncComponent({
  // 加载函数
  loader: () => import('./components/HeavyComponent.vue'),
  
  // 加载中显示的组件
  loadingComponent: LoadingComponent,
  
  // 加载失败显示的组件
  errorComponent: ErrorComponent,
  
  // 显示加载组件前的延迟时间,默认 200ms
  delay: 200,
  
  // 超时时间,默认 Infinity
  timeout: 3000
})
</script>

<template>
  <div>
    <AsyncComponent />
  </div>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue'

// 加载组件
const LoadingSpinner = {
  template: `
    <div class="loading">
      <div class="spinner"></div>
      <p>加载中...</p>
    </div>
  `
}

// 错误组件
const ErrorDisplay = {
  template: `
    <div class="error">
      <p>❌ 加载失败</p>
      <button @click="$emit('retry')">重试</button>
    </div>
  `
}

// 异步加载图片画廊组件
const ImageGallery = defineAsyncComponent({
  loader: () => {
    // 模拟网络延迟
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve({
          setup() {
            const images = ref([
              'https://picsum.photos/300/200?random=1',
              'https://picsum.photos/300/200?random=2',
              'https://picsum.photos/300/200?random=3',
              'https://picsum.photos/300/200?random=4',
              'https://picsum.photos/300/200?random=5',
              'https://picsum.photos/300/200?random=6'
            ])
            return { images }
          },
          template: `
            <div class="gallery">
              <h3>图片画廊</h3>
              <div class="gallery-grid">
                <div v-for="(img, index) in images" :key="index" class="gallery-item">
                  <img :src="img" :alt="'图片 ' + (index + 1)" />
                </div>
              </div>
            </div>
          `
        })
      }, 1500)
    })
  },
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,
  timeout: 5000
})

const showGallery = ref(false)
</script>

<template>
  <div class="container">
    <h2>异步组件示例</h2>
    
    <button @click="showGallery = !showGallery" class="toggle-btn">
      {{ showGallery ? '隐藏' : '显示' }}画廊
    </button>
    
    <div v-if="showGallery" class="gallery-container">
      <ImageGallery />
    </div>
  </div>
</template>

<style scoped>
.container {
  max-width: 1000px;
  margin: 20px auto;
  padding: 20px;
}

.toggle-btn {
  padding: 12px 24px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  margin-bottom: 20px;
}

.toggle-btn:hover {
  background-color: #35a372;
}

.gallery-container {
  margin-top: 20px;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
  background-color: #f9f9f9;
}

.loading {
  text-align: center;
  padding: 60px 20px;
}

.spinner {
  width: 50px;
  height: 50px;
  margin: 0 auto 20px;
  border: 5px solid #f3f3f3;
  border-top: 5px solid #42b983;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.error {
  text-align: center;
  padding: 40px;
  color: #f56c6c;
}

.error button {
  padding: 8px 16px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  margin-top: 10px;
}

.gallery {
  animation: fadeIn 0.5s;
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

.gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 20px;
  margin-top: 20px;
}

.gallery-item {
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  transition: transform 0.3s;
}

.gallery-item:hover {
  transform: translateY(-5px);
  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}

.gallery-item img {
  width: 100%;
  height: 200px;
  object-fit: cover;
  display: block;
}
</style>

Practical example: tabs loaded on demand

<script setup>
import { ref, defineAsyncComponent } from 'vue'

const currentTab = ref('home')

// 定义加载组件
const TabLoading = {
  template: `
    <div style="padding: 40px; text-align: center;">
      <div class="spinner"></div>
      <p>加载标签页...</p>
    </div>
  `
}

// 异步加载各个标签页组件
const tabs = {
  home: defineAsyncComponent({
    loader: () => Promise.resolve({
      template: `
        <div class="tab-content">
          <h3>🏠 首页</h3>
          <p>欢迎来到首页!</p>
          <div class="content-box">
            <h4>最新动态</h4>
            <ul>
              <li>新功能上线</li>
              <li>系统维护通知</li>
              <li>用户反馈</li>
            </ul>
          </div>
        </div>
      `
    }),
    loadingComponent: TabLoading,
    delay: 200
  }),
  
  dashboard: defineAsyncComponent({
    loader: () => new Promise(resolve => {
      setTimeout(() => {
        resolve({
          setup() {
            const stats = ref([
              { label: '总用户', value: '1,234', icon: '👥' },
              { label: '今日访问', value: '567', icon: '📊' },
              { label: '活跃用户', value: '890', icon: '🔥' }
            ])
            return { stats }
          },
          template: `
            <div class="tab-content">
              <h3>📊 仪表盘</h3>
              <div class="stats-grid">
                <div v-for="stat in stats" :key="stat.label" class="stat-card">
                  <div class="stat-icon">{{ stat.icon }}</div>
                  <div class="stat-value">{{ stat.value }}</div>
                  <div class="stat-label">{{ stat.label }}</div>
                </div>
              </div>
            </div>
          `
        })
      }, 800)
    }),
    loadingComponent: TabLoading,
    delay: 200
  }),
  
  analytics: defineAsyncComponent({
    loader: () => new Promise(resolve => {
      setTimeout(() => {
        resolve({
          setup() {
            const data = ref({
              pageViews: 12345,
              uniqueVisitors: 5678,
              bounceRate: '45%',
              avgDuration: '3:24'
            })
            return { data }
          },
          template: `
            <div class="tab-content">
              <h3>📈 分析</h3>
              <div class="analytics-grid">
                <div class="metric">
                  <div class="metric-label">页面浏览量</div>
                  <div class="metric-value">{{ data.pageViews }}</div>
                </div>
                <div class="metric">
                  <div class="metric-label">独立访客</div>
                  <div class="metric-value">{{ data.uniqueVisitors }}</div>
                </div>
                <div class="metric">
                  <div class="metric-label">跳出率</div>
                  <div class="metric-value">{{ data.bounceRate }}</div>
                </div>
                <div class="metric">
                  <div class="metric-label">平均时长</div>
                  <div class="metric-value">{{ data.avgDuration }}</div>
                </div>
              </div>
            </div>
          `
        })
      }, 1000)
    }),
    loadingComponent: TabLoading,
    delay: 200
  }),
  
  settings: defineAsyncComponent({
    loader: () => new Promise(resolve => {
      setTimeout(() => {
        resolve({
          setup() {
            const config = ref({
              siteName: '我的网站',
              language: 'zh-CN',
              timezone: 'Asia/Shanghai',
              notifications: true
            })
            return { config }
          },
          template: `
            <div class="tab-content">
              <h3>⚙️ 设置</h3>
              <div class="settings-form">
                <div class="form-group">
                  <label>网站名称</label>
                  <input v-model="config.siteName" />
                </div>
                <div class="form-group">
                  <label>语言</label>
                  <select v-model="config.language">
                    <option value="zh-CN">简体中文</option>
                    <option value="en-US">English</option>
                  </select>
                </div>
                <div class="form-group">
                  <label>时区</label>
                  <input v-model="config.timezone" />
                </div>
                <div class="form-group">
                  <label>
                    <input type="checkbox" v-model="config.notifications" />
                    启用通知
                  </label>
                </div>
              </div>
            </div>
          `
        })
      }, 600)
    }),
    loadingComponent: TabLoading,
    delay: 200
  })
}

const tabList = [
  { id: 'home', label: '首页', icon: '🏠' },
  { id: 'dashboard', label: '仪表盘', icon: '📊' },
  { id: 'analytics', label: '分析', icon: '📈' },
  { id: 'settings', label: '设置', icon: '⚙️' }
]
</script>

<template>
  <div class="async-tabs">
    <h2>按需加载的标签页</h2>
    
    <div class="tabs-nav">
      <button
        v-for="tab in tabList"
        :key="tab.id"
        :class="['tab-btn', { active: currentTab === tab.id }]"
        @click="currentTab = tab.id"
      >
        <span class="tab-icon">{{ tab.icon }}</span>
        <span>{{ tab.label }}</span>
      </button>
    </div>
    
    <div class="tabs-content">
      <KeepAlive>
        <component :is="tabs[currentTab]" />
      </KeepAlive>
    </div>
  </div>
</template>

<style scoped>
.async-tabs {
  max-width: 900px;
  margin: 20px auto;
  padding: 20px;
}

.tabs-nav {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
  border-bottom: 2px solid #ddd;
}

.tab-btn {
  padding: 12px 20px;
  background: none;
  border: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 8px;
  color: #666;
  transition: all 0.3s;
  border-bottom: 3px solid transparent;
}

.tab-btn:hover {
  color: #42b983;
  background-color: #f5f5f5;
}

.tab-btn.active {
  color: #42b983;
  font-weight: bold;
  border-bottom-color: #42b983;
}

.tab-icon {
  font-size: 18px;
}

.tabs-content {
  min-height: 300px;
  padding: 20px;
  background-color: white;
  border: 1px solid #ddd;
  border-radius: 8px;
}

.tab-content {
  animation: slideIn 0.3s;
}

@keyframes slideIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.spinner {
  width: 40px;
  height: 40px;
  margin: 0 auto;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #42b983;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.content-box {
  margin-top: 20px;
  padding: 20px;
  background-color: #f9f9f9;
  border-radius: 8px;
}

.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
  margin-top: 20px;
}

.stat-card {
  padding: 30px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  border-radius: 12px;
  text-align: center;
}

.stat-icon {
  font-size: 36px;
  margin-bottom: 10px;
}

.stat-value {
  font-size: 32px;
  font-weight: bold;
  margin-bottom: 5px;
}

.stat-label {
  font-size: 14px;
  opacity: 0.9;
}

.analytics-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
  margin-top: 20px;
}

.metric {
  padding: 20px;
  background-color: #f9f9f9;
  border-radius: 8px;
  border-left: 4px solid #42b983;
}

.metric-label {
  font-size: 14px;
  color: #666;
  margin-bottom: 8px;
}

.metric-value {
  font-size: 28px;
  font-weight: bold;
  color: #42b983;
}

.settings-form {
  max-width: 500px;
}

.form-group {
  margin-bottom: 20px;
}

.form-group label {
  display: block;
  margin-bottom: 8px;
  font-weight: bold;
  color: #333;
}

.form-group input[type="text"],
.form-group input:not([type="checkbox"]),
.form-group select {
  width: 100%;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.form-group label:has(input[type="checkbox"]) {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: normal;
}
</style>

Used in conjunction with routing

Using asynchronous components in Vue Router:

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'Home',
      component: () => import('../views/Home.vue')
    },
    {
      path: '/about',
      name: 'About',
      component: () => import('../views/About.vue')
    },
    {
      path: '/dashboard',
      name: 'Dashboard',
      component: () => import('../views/Dashboard.vue')
    }
  ]
})

export default router

Advantages of asynchronous components

1. Reduce initial loading time

<script setup>
// ❌ 同步导入 - 所有组件都会被打包到主bundle
import HeavyComponent1 from './HeavyComponent1.vue'
import HeavyComponent2 from './HeavyComponent2.vue'
import HeavyComponent3 from './HeavyComponent3.vue'

// ✅ 异步导入 - 按需加载
const HeavyComponent1 = defineAsyncComponent(() => import('./HeavyComponent1.vue'))
const HeavyComponent2 = defineAsyncComponent(() => import('./HeavyComponent2.vue'))
const HeavyComponent3 = defineAsyncComponent(() => import('./HeavyComponent3.vue'))
</script>

2. Code splitting

Webpack/Vite will automatically split asynchronous components into independent chunks:

dist/
  ├── index.html
  ├── assets/
  │   ├── index-abc123.js      # 主bundle
  │   ├── HeavyComponent1-def456.js  # 异步chunk
  │   ├── HeavyComponent2-ghi789.js  # 异步chunk
  │   └── HeavyComponent3-jkl012.js  # 异步chunk

3. Improve user experience

  • Faster first screen loading
  • Load resources on demand
  • Better performance

Best Practices

1. Proper use of asynchronous components

<script setup>
import { defineAsyncComponent } from 'vue'

// ✅ 适合异步加载的场景:
// - 大型组件(图表、编辑器等)
// - 条件渲染的组件
// - 路由组件
// - 模态框、抽屉等

// ❌ 不适合异步加载的场景:
// - 小型、常用的组件
// - 首屏必需的组件
</script>

2. Provide loading status

<script setup>
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,  // ✅ 提供加载组件
  delay: 200,  // ✅ 延迟显示加载状态
  timeout: 3000  // ✅ 设置超时时间
})
</script>

3. Error handling

<script setup>
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent({
  loader: () => import('./Component.vue'),
  errorComponent: ErrorDisplay,  // ✅ 提供错误组件
  onError(error, retry, fail, attempts) {
    if (attempts <= 3) {
      retry()  // 重试
    } else {
      fail()  // 失败
    }
  }
})
</script>

Summarize

  • Asynchronous components passdefineAsyncComponentdefinition
  • Configurable loading status, error handling and timeouts
  • withKeepAliveUsed together to cache loaded components
  • Suitable for large components, conditional rendering components and routing components
  • Can significantly reduce initial loading time and improve performance
  • Cooperate with Webpack/Vite to achieve automatic code splitting

Next step

Next, learn Mixins to learn how to reuse component logic (note: Vue 3 recommends using combined functions).