Skip to content
← All questions
Intermediate

What are async components and defineAsyncComponent?

ComponentsPerformance

Async components let you split your bundle by loading a component's code only when it's needed. Instead of importing the component at the top of the file (which bundles it with the parent), you wrap the import in defineAsyncComponent and Vite creates a separate chunk for it.

Basic usage

ts
import { defineAsyncComponent } from 'vue'

const AdminPanel = defineAsyncComponent(() => import('./AdminPanel.vue'))

AdminPanel behaves like a normal component in templates, but its code is only downloaded from the server when the component first renders.

Loading and error states

The full options object lets you control what the user sees while the component loads or if it fails.

ts
import { defineAsyncComponent } from 'vue'
import LoadingSpinner from './LoadingSpinner.vue'
import ErrorDisplay from './ErrorDisplay.vue'

const AsyncDashboard = defineAsyncComponent({
  loader: () => import('./Dashboard.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,
  timeout: 30000
})
OptionPurpose
loaderThe dynamic import function
loadingComponentShown while loading
delayMilliseconds before showing the loading component (default: 200)
errorComponentShown if the import fails or times out
timeoutMilliseconds before treating it as a failure

The delay prevents a spinner from flashing for components that load quickly. Keep it around 200ms unless you have a reason to change it.

When to use async components

They make the biggest difference when the component is large and not always needed:

ts
// Heavy component behind a condition
const ChartEditor = defineAsyncComponent(() => import('./ChartEditor.vue'))

// Route-level splitting (Vue Router does this automatically)
const routes = [
  { path: '/admin', component: () => import('./views/Admin.vue') }
]

Don't wrap small, always-visible components. The overhead of a separate network request outweighs the bundle savings.

Lazy hydration (Vue 3.5+, SSR)

In SSR apps, async components render on the server but can delay hydration on the client until they're actually needed.

ts
import {
  defineAsyncComponent,
  hydrateOnVisible,
  hydrateOnIdle,
  hydrateOnInteraction
} from 'vue'

// Hydrate when the user scrolls to it
const Comments = defineAsyncComponent({
  loader: () => import('./Comments.vue'),
  hydrate: hydrateOnVisible({ rootMargin: '100px' })
})

// Hydrate during idle time
const Footer = defineAsyncComponent({
  loader: () => import('./Footer.vue'),
  hydrate: hydrateOnIdle(5000)
})

// Hydrate on first interaction
const SearchPanel = defineAsyncComponent({
  loader: () => import('./SearchPanel.vue'),
  hydrate: hydrateOnInteraction(['focus', 'click'])
})

This reduces the JavaScript the browser has to process before the page becomes interactive.

Async components vs route-level splitting

defineAsyncComponentRoute lazy loading
ScopeAny componentRoute-level views
SetupManualBuilt into Vue Router
Loading UIloadingComponent optionRouter navigation guards
Use caseConditional UI, heavy widgetsPage-level code splitting

Route-level splitting (() => import('./views/Page.vue')) is the most common form of code splitting. defineAsyncComponent is for splitting within a page.

Released under the MIT License.