Async Components and Suspense
Async Components
Async components let you load components lazily — only when they're needed. This reduces the initial bundle size and improves page load performance. Vue's defineAsyncComponent() wraps a dynamic import and handles loading/error states.
<template>
<div>
<!-- Async component — loaded only when rendered -->
<HeavyChart v-if="showChart" />
<button @click="showChart = !showChart">Toggle Chart</button>
<!-- With loading and error states -->
<AdminPanel v-if="isAdmin" />
</div>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue'
// Simple async component — just a dynamic import
const HeavyChart = defineAsyncComponent(() =>
import('./HeavyChart.vue')
)
// With options — loading state, error state, timeout
const AdminPanel = defineAsyncComponent({
// The loader function
loader: () => import('./AdminPanel.vue'),
// Component to show while loading
loadingComponent: {
template: '<div class="loading-spinner">Loading...</div>'
},
// Delay before showing loading component (ms)
delay: 200,
// Component to show if loading fails
errorComponent: {
template: '<div class="error">Failed to load component</div>'
},
// Timeout — show error if loading takes too long
timeout: 5000,
// Called when loading fails
onError(error, retry, fail, attempts) {
if (attempts <= 3) {
retry() // retry up to 3 times
} else {
fail()
}
}
})
const showChart = ref(false)
const isAdmin = ref(true)
</script>
<!-- Suspense — handle async setup() in child components -->
<template>
<div>
<!-- Suspense wraps async components -->
<Suspense>
<!-- Default slot: shown when ready -->
<template #default>
<AsyncUserProfile :userId="userId" />
</template>
<!-- Fallback slot: shown while loading -->
<template #fallback>
<div class="skeleton">
<div class="skeleton-avatar"></div>
<div class="skeleton-text"></div>
<div class="skeleton-text short"></div>
</div>
</template>
</Suspense>
<!-- Combine with Transition for smooth loading -->
<Suspense @pending="onPending" @resolve="onResolve" @fallback="onFallback">
<template #default>
<Transition name="fade" mode="out-in">
<AsyncDashboard :key="currentPage" />
</Transition>
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</div>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue'
const AsyncUserProfile = defineAsyncComponent(() => import('./UserProfile.vue'))
const AsyncDashboard = defineAsyncComponent(() => import('./Dashboard.vue'))
const userId = ref(1)
const currentPage = ref('home')
function onPending() { console.log('Loading started') }
function onResolve() { console.log('Loading complete') }
function onFallback() { console.log('Showing fallback') }
</script>
<!-- AsyncUserProfile.vue — uses async setup() -->
<!-- <script setup> -->
<!-- const props = defineProps({ userId: Number }) -->
<!-- // await in setup() — Suspense waits for this -->
<!-- const user = await fetch(`/api/users/${props.userId}`).then(r => r.json()) -->
<!-- </script> -->
Ready to Level Up Your Skills?
Explore 500+ free tutorials across 20+ languages and frameworks.