Vue.js course Β· Module 8: Slots & Dynamic Components
Async Components & Suspense
In this lesson5
In NOVA LAB, heavy cargo modules are not loaded right away - the system loads them on demand while showing a status on the screen. Think of a panel with a three-dimensional crater map: if its code ends up in the application's main bundle, every crew member pays for it with startup time, even if they never open the map. Vue offers async components - loading components lazily (on demand) - and Suspense - control over the loading state.
defineAsyncComponent
Instead of importing a component directly, you can load it asynchronously. The defineAsyncComponent function takes a loader function that returns a Promise with the component - most often a dynamic import(), which lets a bundler such as Vite split the component into a separate file:
1<script setup>
2import { defineAsyncComponent } from 'vue'
3
4// Simple lazy import
5const HeavyModule = defineAsyncComponent(
6 () => import('./HeavyModule.vue')
7)
8</script>
9
10<template>
11 <HeavyModule />
12</template>The component is fetched only when it is needed, that is on its first render in the template. To the template, HeavyModule looks like a regular component, accepts props and emits events - only the moment the code is downloaded has changed.
Advanced Configuration
Instead of a bare function you can pass an options object. Besides loader it defines what to show while loading, after how long to show it and what to display when the module transport fails:
1<script setup>
2import { defineAsyncComponent } from 'vue'
3import LoadingSpinner from './LoadingSpinner.vue'
4import ErrorDisplay from './ErrorDisplay.vue'
5
6const HeavyModule = defineAsyncComponent({
7 // Loader function
8 loader: () => import('./HeavyModule.vue'),
9
10 // Component displayed during loading
11 loadingComponent: LoadingSpinner,
12
13 // Delay before showing loading component (ms)
14 delay: 200,
15
16 // Component displayed when loading fails
17 errorComponent: ErrorDisplay,
18
19 // Timeout - after this time the error component is shown (ms)
20 timeout: 10000
21})
22</script>The sequence of events looks like this: Vue calls loader, waits for delay, and if the module still hasn't arrived, it shows loadingComponent. When the import finishes, the spinner is replaced with the actual component. The value of 200 ms is in fact the default delay - on a fast connection the spinner would only flash for a split second. The default timeout is infinite, so without this option errorComponent appears only when the import fails.
Suspense
<Suspense> is a built-in Vue component for managing the loading state of a whole tree of async components at once. Note: in Vue 3.5 it is still marked as an experimental feature - its API may still change, and Vue reminds you of that with a console message. It has two slots, #default and #fallback:
1<template>
2 <Suspense>
3 <!-- Main content (loaded component) -->
4 <template #default>
5 <HeavyModule />
6 </template>
7
8 <!-- Loading state -->
9 <template #fallback>
10 <div class="loading">
11 <p>Loading module...</p>
12 <div class="spinner"></div>
13 </div>
14 </template>
15 </Suspense>
16</template>On the initial render, Suspense renders the #default content in memory. That is when the async component starts loading, and Suspense enters the pending state and displays #fallback. When all async dependencies have finished, it switches to #default. An important detail: inside Suspense, the async component's own loadingComponent, errorComponent, delay and timeout options are ignored, because Suspense controls the loading state. If a component should keep its own spinner, give it the suspensible: false option.
Suspense Events
Suspense emits events that report the loading state: pending when it enters the pending state, fallback when the fallback content is displayed and resolve when the main content is ready:
1<template>
2 <Suspense
3 @pending="onPending"
4 @resolve="onResolve"
5 @fallback="onFallback"
6 >
7 <template #default>
8 <AsyncModule />
9 </template>
10 <template #fallback>
11 <LoadingSpinner />
12 </template>
13 </Suspense>
14</template>
15
16<script setup>
17const onPending = () => console.log('Loading started...')
18const onResolve = () => console.log('Loading complete!')
19const onFallback = () => console.log('Fallback displayed')
20</script>On the first load the console shows "Loading started...", "Fallback displayed" and "Loading complete!" in that order. This is a good place to measure loading time or add an entry to the station log.
Async Setup in Components
A component with async setup or a top-level await in <script setup> automatically becomes an async dependency for Suspense:
1<!-- AsyncModule.vue -->
2<script setup>
3const data = await fetch('/api/module-data')
4 .then(r => r.json())
5</script>
6
7<template>
8 <div>{{ data.name }}</div>
9</template>Such a component must have a <Suspense> above it. Without one, Vue prints a warning and doesn't render it at all. Suspense also doesn't handle errors by itself - you catch a rejected fetch in a parent component with the onErrorCaptured hook.
My advice: in production code, start with defineAsyncComponent with loadingComponent and errorComponent, because it is a stable API. Use Suspense where several components load at once and you want one shared waiting screen, keeping its experimental status in mind. In the editor below, a button loads HeavyModule with a simulated two-second delay, and after 200 ms LoadingSpinner appears.
Remember: a heavy module flies to the station only when someone needs it, and Suspense keeps the waiting screen up until the whole cargo has arrived.
Code for this lesson: App.vue
1<script setup>
2import { defineAsyncComponent, ref } from 'vue'
3import LoadingSpinner from './LoadingSpinner.vue'
4
5// Async component simulation (in a real app: import('./Heavy.vue'))
6const HeavyModule = defineAsyncComponent({
7 loader: () => new Promise((resolve) => {
8 setTimeout(() => {
9 resolve({
10 template: `
11 <div class="heavy-module">
12 <h2>Heavy Module Loaded!</h2>
13 <p>Complex data visualization ready</p>
14 <div class="chart">
15 <div v-for="h in [80, 60, 90, 45, 75]" :key="h"
16 class="bar" :style="{ height: h + 'px' }">
17 </div>
18 </div>
19 </div>
20 `
21 })
22 }, 2000)
23 }),
24 loadingComponent: LoadingSpinner,
25 delay: 200
26})
27
28const showModule = ref(false)
29</script>
30
31<template>
32 <div class="nova-lab">
33 <h1>Async Components & Suspense</h1>
34
35 <button @click="showModule = !showModule" class="btn">
36 {{ showModule ? 'Hide' : 'Load' }} Heavy Module
37 </button>
38
39 <div v-if="showModule" class="container">
40 <HeavyModule />
41 </div>
42 </div>
43</template>
44
45<style scoped>
46.nova-lab {
47 background: #0a0e27;
48 color: #00ff88;
49 min-height: 100vh;
50 padding: 20px;
51 font-family: monospace;
52}
53h1 { color: #00ff88; margin-bottom: 20px; }
54.btn {
55 background: #00b4d8;
56 color: #0a0e27;
57 border: none;
58 padding: 12px 24px;
59 cursor: pointer;
60 font-family: monospace;
61 font-weight: bold;
62 font-size: 14px;
63}
64.container {
65 margin-top: 20px;
66 border: 2px solid #00b4d8;
67 padding: 20px;
68}
69</style>
70
71<style>
72.heavy-module h2 { color: #00ff88; margin-bottom: 10px; }
73.heavy-module .chart {
74 display: flex;
75 align-items: flex-end;
76 gap: 8px;
77 margin-top: 15px;
78 height: 100px;
79}
80.heavy-module .bar {
81 width: 30px;
82 background: #00b4d8;
83}
84</style>Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is defineAsyncComponent used for in Vue?
2. Which slot in <Suspense> displays content during loading?
Hands-on tasks in the game
- Vertical ordering
Order the stages of how Suspense works:
- Code editor
Use defineAsyncComponent for lazy loading the HeavyModule component. Display 'Loading...' during loading.
- Horizontal ordering
Arrange the code for defining an async component:
- Click in order
Arrange the Suspense code with an async component:
- Code editor
Use <Suspense> with #default and #fallback slots to display an async component with a loading spinner.