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.