Async Components

defineAsyncComponent loads a component on demand, with its own loading and error states and a bundle chunk of its own.

SyntaxdefineAsyncComponent(loader | { loader, loadingComponent, errorComponent, delay, timeout, suspensible, onError })

An async component is a component whose code is not in the JavaScript the browser downloads first. defineAsyncComponent takes a loader: a function that returns a promise for a component. Vue calls the loader the first time that component is about to render, then caches the result, so the second render does not fetch again.

The two forms

Passing the loader on its own is shorthand. These are the same thing:

const Chart = defineAsyncComponent(() => import('./SalesChart.vue'))

const Chart = defineAsyncComponent({
  loader: () => import('./SalesChart.vue')
})

The object form is what the shorthand expands to, plus the options below.

Loading, error, delay, timeout

loadingComponent renders while the promise is pending and errorComponent renders if it rejects. delay is how long Vue waits before showing the loading component, and it defaults to 200ms so a fast network does not produce a flash of spinner. timeout defaults to Infinity; set it in milliseconds and a slower load is treated as an error.

What trips people up

Inside a <Suspense> boundary an async component is a suspense dependency by default, and its own loading, error, delay and timeout options are ignored because the boundary owns that state. suspensible: false opts out. Separately, a build in Vite names each chunk with a content hash: after a deploy an open tab can request a chunk that no longer exists, the loader rejects, and the error component is what the user sees until they reload.

Example

Example · vue
<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue'
import ChartLoading from './ChartLoading.vue'
import ChartError from './ChartError.vue'

const SalesChart = defineAsyncComponent({
  loader: () => import('./SalesChart.vue'),
  loadingComponent: ChartLoading,
  errorComponent: ChartError,
  delay: 200,
  timeout: 10000
})

const showChart = ref(false)
</script>

<template>
  <button type="button" @click="showChart = true">Show sales chart</button>
  <sales-chart v-if="showChart" />
</template>

<style scoped>
button {
  font: inherit;
}
</style>

When to use it

  • Keep a charting library out of the first download and fetch it when the user opens the reports panel.
  • Show a skeleton for a slow-loading panel and an error card when its chunk fails.
  • Retry a component chunk that failed on a flaky mobile connection before giving up.

More examples

Retrying a chunk that failed to download

onError receives the error, a retry function, a fail function and the attempt count. Calling neither retry nor fail leaves the component in its loading state forever, so one of them must run on every path. This is the right place to handle the flaky-network case; a stale chunk after a deploy will fail all three times, so pair it with a prompt to reload.

Example · ts
// src/components/lazy-report.ts
import { defineAsyncComponent } from 'vue'

export const LazyReport = defineAsyncComponent({
  loader: () => import('./ReportTable.vue'),
  timeout: 8000,
  onError(error: Error, retry: () => void, fail: () => void, attempts: number) {
    if (attempts <= 2) {
      retry()
      return
    }
    console.warn('report chunk failed', error.message)
    fail()
  }
})

// attempts is 1 on the first failure, so this tries the download three times
// in total, then renders nothing (no errorComponent was given).
// Output: report chunk failed Failed to fetch dynamically imported module

Deferring hydration on a server-rendered page (Vue 3.5+)

Hydration is the step where client-side Vue takes over server-rendered HTML and makes it interactive. The lazy hydration strategies were added in Vue 3.5: hydrateOnVisible, hydrateOnIdle, hydrateOnMediaQuery and hydrateOnInteraction. They only apply when the markup came from a server render, so on a plain client-rendered app they do nothing useful.

Example · ts
// src/components/lazy-comments.ts
import { defineAsyncComponent, hydrateOnVisible, hydrateOnIdle } from 'vue'

// Download and attach listeners only once the section scrolls near the viewport.
export const Comments = defineAsyncComponent({
  loader: () => import('./CommentList.vue'),
  hydrate: hydrateOnVisible({ rootMargin: '100px' })
})

// Or wait for the main thread to go quiet, with a millisecond ceiling.
export const Recommendations = defineAsyncComponent({
  loader: () => import('./Recommendations.vue'),
  hydrate: hydrateOnIdle(2000)
})

Registering one globally for use anywhere

A globally registered async component is usable in any template as <markdown-editor> without an import, and its code still only downloads when something renders it. Reach for this when a heavy widget appears in many unrelated places; prefer a local import otherwise, because global registration hides where the dependency comes from.

Example · ts
// src/main.ts
import { createApp, defineAsyncComponent } from 'vue'
import App from './App.vue'

const app = createApp(App)

app.component(
  'MarkdownEditor',
  defineAsyncComponent(() => import('./components/MarkdownEditor.vue'))
)

app.mount('#app')

Discussion

  • Be the first to comment on this lesson.