Async Components

This section documents the current Vue 3.x release line as published at the official Vue.js documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. The Composition API with <script setup> is the authoring style used throughout; the Options API is shown only as a contrast.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vue and its ecosystem iterate quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

An async component is defined by a function that returns a Promise resolving to a component. Vue does not request its code until the component is first rendered, which lets a bundler split it into a separate chunk loaded on demand.

defineAsyncComponent

Wrap a dynamic import() in defineAsyncComponent. The result is a normal component you register and use like any other:

import { defineAsyncComponent } from 'vue'

const AsyncDashboard = defineAsyncComponent(() =>
  import('./components/Dashboard.vue')
)
<script setup>
import { defineAsyncComponent } from 'vue'

const AsyncDashboard = defineAsyncComponent(() => import('./components/Dashboard.vue'))
</script>

<template>
  <AsyncDashboard v-if="showDashboard" />
</template>

The loader runs once; the resolved component is cached for every later use. See Async Components and defineAsyncComponent().

The options object

Passing an options object instead of a bare loader controls what renders while loading and on failure:

const AsyncDashboard = defineAsyncComponent({
  loader: () => import('./components/Dashboard.vue'),

  loadingComponent: LoadingSpinner,
  delay: 200,          // ms to wait before showing loadingComponent (default 200)

  errorComponent: LoadError,
  timeout: 8000,       // ms after which the load counts as failed (default: never)

  onError(error, retry, fail, attempts) {
    if (error.message.includes('fetch') && attempts <= 3) {
      retry()          // re-run the loader
    } else {
      fail()
    }
  }
})
  • delay prevents a spinner flash on fast connections — loadingComponent appears only if the load is still pending after that many milliseconds.

  • timeout triggers errorComponent (and the onError handler) if the loader has not resolved in time.

  • onError receives retry and fail callbacks plus the attempt count, giving a bounded retry loop for transient network errors.

Route-level code splitting

A router is the most common split point: give a route a lazy component and that route’s code — with its child components — forms its own chunk, fetched when the user first navigates there.

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: () => import('./views/Home.vue') },
    { path: '/reports', component: () => import('./views/Reports.vue') }
  ]
})

Vue Router accepts the () ⇒ import(…​) loader directly, so defineAsyncComponent is not needed for route components — it is for lazily loaded components used inside a view. See Lazy Loading Routes.

Pairing with Suspense

By default each async component manages its own loading and error UI. Wrapping one — or a tree of them — in the experimental <Suspense> built-in hoists that to a single boundary: one fallback while any nested async dependency (async components, and components with an async setup()) is still pending.

<template>
  <Suspense>
    <template #default>
      <AsyncDashboard />
    </template>
    <template #fallback>
      <p>Loading dashboard...</p>
    </template>
  </Suspense>
</template>

Under <Suspense> a component’s own loadingComponent is ignored — the #fallback template takes over — while errorComponent still handles a failed load and an onErrorCaptured hook on a parent can catch what is left. <Suspense>, <KeepAlive>, and <Teleport> are covered together in KeepAlive, Teleport, and Suspense. See Suspense.

stateDiagram-v2 [*] --> Pending: component first rendered Pending --> FallbackShown: Suspense renders the fallback slot FallbackShown --> Resolved: loader promise resolves FallbackShown --> Errored: loader rejects, or timeout elapses Resolved --> [*]: default slot content shown Errored --> Pending: onError calls retry() Errored --> [*]: onError calls fail(), errorComponent shown

See also