Skip to content
← All questions
⚡ Intermediate

How does Suspense work for async components?

🧩 Components🎰 Slots⏳ Suspense🌀 Teleport

<Suspense> is a built-in component that renders fallback content while its async children resolve. It works with two kinds of async dependencies: async components (defineAsyncComponent) and components with an async setup().

Basic usage

vue
<template>
  <Suspense>
    <!-- Default slot: the async content -->
    <AsyncDashboard />

    <!-- Fallback slot: shown while loading -->
    <template #fallback>
      <LoadingSkeleton />
    </template>
  </Suspense>
</template>
Open in Vue Playground

Suspense waits for all async dependencies inside the default slot to resolve before swapping from the fallback to the real content.

Async setup

A component with async setup() (using top-level await in <script setup>) is automatically a Suspense dependency:

vue
<!-- UserProfile.vue -->
<script setup>
const user = await fetchUser() // top-level await
const posts = await fetchPosts(user.id)
</script>

<template>
  <h1>{{ user.name }}</h1>
  <PostList :posts="posts" />
</template>
Open in Vue Playground
vue
<!-- Parent.vue -->
<template>
  <Suspense>
    <UserProfile />
    <template #fallback>Loading profile...</template>
  </Suspense>
</template>
Open in Vue Playground

Single root in both slots

Suspense tracks one immediate child per slot. Wrap multiple elements:

vue
<template>
  <Suspense>
    <div>
      <AsyncHeader />
      <AsyncContent />
    </div>

    <template #fallback>
      <div>
        <LoadingSpinner />
        <p>Loading...</p>
      </div>
    </template>
  </Suspense>
</template>
Open in Vue Playground

Timeout for re-triggers

When Suspense is already resolved and new async work starts (e.g., switching views), the old content stays visible until a timeout elapses. Set timeout to control when the fallback reappears:

vue
<template>
  <!-- Show fallback after 200ms if the new view hasn't resolved -->
  <Suspense :timeout="200">
    <component :is="currentView" :key="currentView" />
    <template #fallback>Loading...</template>
  </Suspense>
</template>
Open in Vue Playground

Suspense events

Track loading state programmatically with @pending, @resolve, and @fallback:

vue
<script setup>
import { ref } from 'vue'
const isLoading = ref(false)
</script>

<template>
  <ProgressBar v-if="isLoading" />

  <Suspense @pending="isLoading = true" @resolve="isLoading = false">
    <AsyncPage />
    <template #fallback><PageSkeleton /></template>
  </Suspense>
</template>
Open in Vue Playground

Nesting with RouterView, Transition, KeepAlive

The correct nesting order is RouterView, then Transition, then KeepAlive, then Suspense:

vue
<template>
  <RouterView v-slot="{ Component }">
    <Transition mode="out-in">
      <KeepAlive>
        <Suspense>
          <component :is="Component" />
          <template #fallback>Loading...</template>
        </Suspense>
      </KeepAlive>
    </Transition>
  </RouterView>
</template>
Open in Vue Playground

Suspense is still experimental

Suspense is still an experimental API. It works, but the API could change in future releases. In production, keep Suspense boundaries minimal and document where you use them.

See also: What are async components? · What are Teleport, Fragments, and Suspense? · How does error handling work?

References

Released under the MIT License.