async components and suspense in Vue.js
Async Components
Async components are loaded on demand (lazy-loaded), reducing the initial bundle size. Vue provides defineAsyncComponent for this purpose.
defineAsyncComponent
typescript
import { defineAsyncComponent } from 'vue'
// Basic async component
const AsyncModal = defineAsyncComponent(
() => import('./components/HeavyModal.vue')
)
// With options
const AsyncDashboard = defineAsyncComponent({
loader: () => import('./components/Dashboard.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200, // Delay before showing loading (ms)
timeout: 10000, // Timeout before showing error (ms)
})vue
<template>
<!-- Loaded only when rendered -->
<AsyncModal v-if="showModal" />
<AsyncDashboard />
</template>Suspense
<Suspense> is a built-in component for handling async dependencies — it shows fallback content while async components or async setup are loading.
vue
<template>
<Suspense>
<!-- Default slot: async content -->
<template #default>
<AsyncDashboard />
</template>
<!-- Fallback slot: shown while loading -->
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>Async Setup
Components with async setup work with <Suspense>:
vue
<!-- UserProfile.vue -->
<script setup>
// This makes the component async — needs Suspense parent
const response = await fetch('/api/user/1')
const user = await response.json()
</script>
<template>
<div>
<h1>{{ user.name }}</h1>
<p>{{ user.email }}</p>
</div>
</template>vue
<!-- Parent.vue -->
<template>
<Suspense>
<UserProfile />
<template #fallback>
<p>Loading user...</p>
</template>
</Suspense>
</template>Suspense Events
vue
<Suspense
@pending="onPending"
@resolve="onResolve"
@fallback="onFallback"
>
<AsyncComponent />
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>Nested Suspense
vue
<Suspense>
<template #default>
<Dashboard>
<!-- Inner Suspense for independent loading -->
<Suspense>
<template #default><SlowWidget /></template>
<template #fallback><WidgetSkeleton /></template>
</Suspense>
</Dashboard>
</template>
<template #fallback>
<DashboardSkeleton />
</template>
</Suspense>Combining with Vue Router
vue
<template>
<RouterView v-slot="{ Component }">
<Suspense>
<template #default>
<component :is="Component" />
</template>
<template #fallback>
<PageLoading />
</template>
</Suspense>
</RouterView>
</template>Error Handling
vue
<script setup>
import { onErrorCaptured, ref } from 'vue'
const error = ref<Error | null>(null)
onErrorCaptured((err) => {
error.value = err
return false // Prevent propagation
})
</script>
<template>
<div v-if="error">Error: {{ error.message }}</div>
<Suspense v-else>
<AsyncComponent />
<template #fallback><Loading /></template>
</Suspense>
</template>Important:
Use defineAsyncComponent for code splitting — load heavy components on demand. Use <Suspense> to show loading states for async components and components with async setup(). Combine both with Vue Router for route-level code splitting. Suspense is still experimental in Vue 3 but widely used.
Short Answer
Interview readyPremium
A concise answer to help you respond confidently on this topic during an interview.