skip to content

In Vue 3, how does `defineAsyncComponent` lazy-load a heavy chart panel, and what do its loadingComponent, delay, errorComponent and timeout options control?

level: middleimportance: should knowfreq 52%

answer

  1. a wrapper around a promise
  2. loader runs on first render
  3. flicker guard before loading UI
  4. error state gets the error
  5. no timeout unless you set one

basics

~20 s

Vue's defineAsyncComponent wraps a loader returning a promise, usually a dynamic import, and calls it only when first rendered. loadingComponent shows after delay (200 ms default); errorComponent shows on rejection or when an optional timeout passes.

solid answer

~40 s

`defineAsyncComponent` returns a wrapper component. Its loader, typically `() => import('./ChartPanel.vue')`, runs only the first time the wrapper renders; the resolved component (or a module's `default` export) is cached, so later instances reuse it. The wrapper forwards props, slots and a template ref to the inner component, so it is a drop-in replacement. With the options object: `loadingComponent` renders while loading, but only after `delay` (default 200 ms, so fast loads do not flash a spinner; `0` shows it at once). `errorComponent` renders when the loader rejects or when `timeout` elapses, and receives the failure as an `error` prop. `timeout` has no default: without it a hung request never becomes an error. `suspensible` (default `true`) hands control to a `<Suspense>` ancestor if there is one.

go deeper

for a junior

Recall the one-line form with a dynamic import and that the loader only runs when the component is actually rendered.

for a middle

Walk through the pending, loading-after-delay, resolved and error states, and name the defaults for delay and timeout.

for a senior

Configure the loading and error views for a real network, including an explicit timeout, and keep wrapper definitions stable so the cache is not thrown away.

for a principal

Decide which heavy panels justify a lazy boundary and a loading state at all, weighing an extra request and UI churn against initial bundle size.

## What `defineAsyncComponent` returns `defineAsyncComponent` is Vue 3's API for a component whose code is fetched on demand. It takes either a **loader function** or an **options object** with a `loader` field, and returns a **wrapper component** you use like any other. The loader must return a promise; in practice it is an ES dynamic import, which bundlers such as Vite treat as a split point: ```ts import { defineAsyncComponent } from 'vue' const ChartPanel = defineAsyncComponent(() => import('./ChartPanel.vue')) ``` Key behaviours of the wrapper: - The loader runs **only when the wrapper is first rendered**, not when `defineAsyncComponent` is called. - The result is **cached** in the wrapper. Every later instance and re-render reuses the resolved component; a second `ChartPanel` on the page does not call the loader again. - A resolved ES module is unwrapped to its `default` export. - **Props, slots and a template ref** are passed through to the inner component, so the wrapper can replace the eager import without changes at the call site. - If the load fails, the cached request is cleared, so the next render of the wrapper tries the loader again. ## The loading and error options | Option | Default | What it controls | |---|---|---| | `loader` | required | Function returning a promise of the component | | `loadingComponent` | none | Component rendered while the load is pending | | `delay` | `200` ms | How long to wait before showing `loadingComponent` | | `errorComponent` | none | Component rendered when the load fails or times out | | `timeout` | none, never times out | Milliseconds after which a pending load counts as failed | | `suspensible` | `true` | Whether a `<Suspense>` ancestor takes control | | `onError` | none | Hook to retry or give up after a failure | ## How the states play out For a **heavy chart panel** behind a Statistics tab, the render sequence without a `<Suspense>` ancestor is: 1. The wrapper mounts and calls the loader. It renders nothing yet (an empty placeholder). 2. If the load is still pending when `delay` expires, it renders `loadingComponent`. The 200 ms default exists because on a fast network an instant spinner is replaced almost at once and looks like a **flicker**. Set `delay: 0` to show it immediately. 3. When the promise resolves, the wrapper renders the real component with the original props and slots. 4. If the promise rejects, or `timeout` elapses first, it renders `errorComponent`, passing the failure as an `error` prop so the error UI can show a message. The error is also reported through Vue's normal error path: `onErrorCaptured` hooks in ancestors, then `app.config.errorHandler`. When no `errorComponent` is provided and nothing handles the error, development builds rethrow it so it is noticed. ## Where to define the wrapper Call `defineAsyncComponent` once, at module level or at the top of `<script setup>`. Calling it inside a `computed`, a render function or a template expression creates a **new component type** on every evaluation, so Vue unmounts and remounts the panel and the wrapper's cache starts empty each time. ## Example options object ```ts import { defineAsyncComponent } from 'vue' import ChartSkeleton from './ChartSkeleton.vue' import ChartLoadError from './ChartLoadError.vue' export const ChartPanel = defineAsyncComponent({ loader: () => import('./ChartPanel.vue'), loadingComponent: ChartSkeleton, delay: 200, errorComponent: ChartLoadError, timeout: 10_000, }) ``` With this configuration a fast load shows the chart with no skeleton, a slow one shows the skeleton after 200 ms, and a request still pending after ten seconds switches to the error view. The `suspensible` flag and `onError` retries change this picture and are worth knowing for production hardening.

  • With Vue 3's `defineAsyncComponent`, what does the user see if the chunk arrives after `timeout` has already shown `errorComponent`?
    The real component. The timeout only flips the wrapper into its error state; the loader promise is not cancelled, and when it later resolves the wrapper marks itself loaded and renders the inner component in place of the error view.
  • Why is calling `defineAsyncComponent` inside a `computed` that picks a chart type a bug?
    Each evaluation returns a brand-new wrapper component, which is a different vnode type, so Vue unmounts the current panel and mounts a new one, and the new wrapper's cache is empty. Define each wrapper once at module or setup level and let the `computed` choose between the existing wrappers.

The delay works like a shop assistant who waits a moment before saying 'one moment, please': if the item arrives straight away you never hear the apology, and only a genuinely slow fetch earns the waiting message.

saying these in an interview costs you the question

  • defineAsyncComponent calls the loader immediately when it is defined.
  • loadingComponent appears the instant rendering starts, with no delay by default.
  • Every instance of the wrapper downloads and resolves the component again.
  • An async component times out after a few seconds by default.
  • The async wrapper drops props and slots, so the inner component must fetch its own data.