<Suspense> is an experimental feature. Its API may change in future releases. Use with caution in production.
The <Suspense> component is used to orchestrate async dependencies in a component tree. It can render a loading state while waiting for multiple nested async dependencies down the component tree to be resolved.
Basic Usage
<Suspense>
<!-- component with nested async dependencies -->
<template #default>
<AsyncComponent />
</template>
<!-- loading state via #fallback slot -->
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
Props
timeout
Specifies the timeout (in milliseconds) before displaying the fallback content. If set to 0, the fallback will be displayed immediately when entering pending state.
<Suspense :timeout="500">
<AsyncComponent />
<template #fallback>
Loading...
</template>
</Suspense>
suspensible
- Type:
boolean
- Default:
false
When true, allows this suspense boundary to be captured by a parent suspense. By default, each <Suspense> is isolated.
<Suspense>
<Suspense suspensible>
<!-- This child suspense can trigger parent's pending state -->
<DeepAsyncComponent />
</Suspense>
<template #fallback>
Loading parent...
</template>
</Suspense>
Events
onResolve
Emitted when the default slot has finished resolving new content.
<Suspense @resolve="onResolve">
<AsyncComponent />
</Suspense>
onPending
Emitted when entering a pending state (when switching to new content that has async dependencies).
<Suspense @pending="onPending">
<component :is="asyncView" />
</Suspense>
onFallback
Emitted when the fallback content is displayed.
<Suspense @fallback="onFallback">
<AsyncComponent />
<template #fallback>
Loading...
</template>
</Suspense>
Slots
default
The primary content to display. If the content has async dependencies, it will enter a “pending” state until all dependencies are resolved.
fallback
The content to display while the default slot is in a pending state.
Async Dependencies
<Suspense> can detect async dependencies in the component tree. An async dependency can be:
1. Async Setup
A component with an async setup() function:
<script setup>
const data = await fetch('/api/data').then(r => r.json())
</script>
2. Async Component
A component loaded asynchronously via defineAsyncComponent():
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => import('./AsyncComp.vue'))
States and Transitions
<Suspense> has three states:
- Pending: Waiting for async dependencies to resolve
- Resolved: Default content is displayed
- Fallback: Fallback content is displayed
Initial Mount
1. Render default content in memory (pending)
2. If async deps > 0:
- Emit @pending
- Emit @fallback
- Render fallback content
3. When all deps resolve:
- Emit @resolve
- Swap in default content
Updating Content
When the default content updates with new async dependencies:
1. Enter pending state (@pending)
2. Continue showing current content OR
After timeout: show fallback (@fallback)
3. When resolved: swap to new content (@resolve)
Nested Async Components Example
<template>
<Suspense>
<Dashboard />
<template #fallback>
<div class="loading">
<Spinner />
<p>Loading dashboard...</p>
</div>
</template>
</Suspense>
</template>
<!-- Dashboard.vue -->
<script setup>
import { ref } from 'vue'
// This component has async dependencies
const userData = await fetch('/api/user').then(r => r.json())
const preferences = await fetch('/api/preferences').then(r => r.json())
</script>
<template>
<div class="dashboard">
<UserProfile :data="userData" />
<UserSettings :preferences="preferences" />
</div>
</template>
Combining with Router
<router-view v-slot="{ Component }">
<Suspense>
<component :is="Component" />
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</router-view>
Error Handling
Async errors in suspense boundaries can be caught using onErrorCaptured:
<script setup>
import { onErrorCaptured, ref } from 'vue'
const error = ref(null)
onErrorCaptured((err) => {
error.value = err
return true // prevent error from propagating
})
</script>
<template>
<div v-if="error" class="error">
Error: {{ error.message }}
</div>
<Suspense v-else>
<AsyncComponent />
<template #fallback>
Loading...
</template>
</Suspense>
</template>
Using with Transitions
<Suspense>
<component :is="asyncView" />
<template #fallback>
<Transition name="fade" mode="out-in">
<LoadingSpinner />
</Transition>
</template>
</Suspense>
Using with KeepAlive
When combining <Suspense> with <KeepAlive>, the <KeepAlive> should be nested inside <Suspense>:
<Suspense>
<KeepAlive>
<component :is="asyncView" />
</KeepAlive>
<template #fallback>
Loading...
</template>
</Suspense>
Timeout Behavior
timeout > 0: Wait for the specified milliseconds before showing fallback
timeout = 0: Show fallback immediately
timeout not set: Only show fallback if component is not resolved synchronously
<!-- Show fallback immediately -->
<Suspense :timeout="0">
<AsyncComponent />
<template #fallback>Loading...</template>
</Suspense>
<!-- Wait 200ms before showing fallback -->
<Suspense :timeout="200">
<AsyncComponent />
<template #fallback>Loading...</template>
</Suspense>
Implementation Details
- Pending Branch: Content is rendered in an off-DOM container until resolved (implementation at
/packages/runtime-core/src/components/Suspense.ts:169)
- Dependency Tracking: Uses a counter to track pending async dependencies (increments when found, decrements when resolved)
- Hydration: Special handling for SSR - assumes server-rendered content is the resolved state
LRU Cache Strategy
When combined with <KeepAlive>, suspense boundaries cache after the suspense resolves, not during the pending state (implementation at /packages/runtime-core/src/components/KeepAlive.ts:249-252).
Server-Side Rendering
During SSR:
- Only the resolved default content is rendered
- Fallback content is not included in SSR output
- On the client, hydration assumes the SSR content represents the resolved state
Limitations and Caveats
- Experimental API: The API is subject to change in future versions
- Single Root: Both default and fallback slots should have a single root node
- Async Detection: Only detects async
setup() and async components, not other async operations in lifecycle hooks
Source Reference
- Package:
@vue/runtime-core
- Implementation:
/packages/runtime-core/src/components/Suspense.ts:62-128
- Props Interface:
/packages/runtime-core/src/components/Suspense.ts:36-47
- Boundary Interface:
/packages/runtime-core/src/components/Suspense.ts:414-444
See Also