Cours Vue.js · Module 8: Slots et composants dynamiques
Composants asynchrones & Suspense
Dans cette leçon5
Au NOVA LAB, les modules de fret lourds ne sont pas chargés tout de suite - le système les charge à la demande tout en affichant un état à l'écran. Pense à un panneau avec une carte des cratères en trois dimensions : si son code se retrouve dans le bundle principal de l'application, chaque membre d'équipage le paie en temps de démarrage, même s'il n'ouvre jamais la carte. Vue propose les composants asynchrones - le chargement paresseux (à la demande) des composants - et Suspense - le contrôle de l'état de chargement.
defineAsyncComponent
Au lieu d'importer un composant directement, tu peux le charger de façon asynchrone. La fonction defineAsyncComponent prend une fonction de chargement qui renvoie une Promise avec le composant - le plus souvent un import() dynamique, grâce auquel un bundler comme Vite place le composant dans un fichier séparé :
1<script setup>
2import { defineAsyncComponent } from 'vue'
3
4// Import paresseux simple
5const HeavyModule = defineAsyncComponent(
6 () => import('./HeavyModule.vue')
7)
8</script>
9
10<template>
11 <HeavyModule />
12</template>Le composant n'est téléchargé que lorsqu'on en a besoin, c'est-à-dire lors de son premier rendu dans le template. Pour le template, HeavyModule ressemble à un composant ordinaire, accepte des props et émet des événements - seul le moment du téléchargement du code a changé.
Configuration avancée
Au lieu d'une simple fonction, tu peux passer un objet d'options. En plus de loader, il définit ce qu'il faut afficher pendant le chargement, après combien de temps l'afficher et ce qu'il faut montrer quand le transport du module échoue :
1<script setup>
2import { defineAsyncComponent } from 'vue'
3import LoadingSpinner from './LoadingSpinner.vue'
4import ErrorDisplay from './ErrorDisplay.vue'
5
6const HeavyModule = defineAsyncComponent({
7 // Fonction de chargement
8 loader: () => import('./HeavyModule.vue'),
9
10 // Composant affiché pendant le chargement
11 loadingComponent: LoadingSpinner,
12
13 // Délai avant d'afficher le composant de chargement (ms)
14 delay: 200,
15
16 // Composant affiché en cas d'échec du chargement
17 errorComponent: ErrorDisplay,
18
19 // Timeout - après ce délai, le composant d'erreur est affiché (ms)
20 timeout: 10000
21})
22</script>La séquence des événements est la suivante : Vue appelle loader, attend delay, et si le module n'est toujours pas arrivé, il affiche loadingComponent. Quand l'import se termine, le spinner est remplacé par le vrai composant. La valeur de 200 ms est d'ailleurs le delay par défaut - sur une connexion rapide, le spinner ne clignoterait qu'une fraction de seconde. Le timeout par défaut est infini, donc sans cette option, errorComponent n'apparaît qu'en cas d'échec de l'import.
Suspense
<Suspense> est un composant intégré de Vue qui gère l'état de chargement de tout un arbre de composants asynchrones à la fois. Attention : dans Vue 3.5, il est toujours marqué comme fonctionnalité expérimentale - son API peut encore changer, et Vue te le rappelle par un message dans la console. Il possède deux slots, #default et #fallback :
1<template>
2 <Suspense>
3 <!-- Contenu principal (composant chargé) -->
4 <template #default>
5 <HeavyModule />
6 </template>
7
8 <!-- État de chargement -->
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>Au premier rendu, Suspense rend le contenu de #default en mémoire. C'est à ce moment que le composant asynchrone commence à se charger, et Suspense passe à l'état d'attente et affiche #fallback. Quand toutes les dépendances asynchrones sont terminées, il bascule vers #default. Détail important : à l'intérieur de Suspense, les options loadingComponent, errorComponent, delay et timeout propres au composant asynchrone sont ignorées, car c'est Suspense qui contrôle l'état de chargement. Si un composant doit garder son propre spinner, donne-lui l'option suspensible: false.
Les événements de Suspense
Suspense émet des événements qui renseignent sur l'état du chargement : pending à l'entrée dans l'état d'attente, fallback à l'affichage du contenu de repli et resolve quand le contenu principal est prêt :
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>Au premier chargement, la console affiche dans l'ordre "Loading started...", "Fallback displayed" et "Loading complete!". C'est le bon endroit pour mesurer le temps de chargement ou ajouter une entrée au journal de la station.
Le setup asynchrone dans les composants
Un composant avec un async setup ou un await au niveau supérieur de <script setup> devient automatiquement une dépendance asynchrone pour 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>Un tel composant doit avoir un <Suspense> au-dessus de lui. Sans cela, Vue affiche un avertissement et ne le rend pas du tout. Suspense ne gère pas non plus les erreurs lui-même - tu captures un fetch rejeté dans un composant parent avec le hook onErrorCaptured.
Mon conseil : dans le code de production, commence par defineAsyncComponent avec loadingComponent et errorComponent, car c'est une API stable. Utilise Suspense là où plusieurs composants se chargent en même temps et où tu veux un seul écran d'attente commun, en gardant à l'esprit son statut expérimental. Dans l'éditeur ci-dessous, un bouton charge HeavyModule avec un délai simulé de deux secondes, et au bout de 200 ms LoadingSpinner apparaît.
Retiens : un module lourd ne vole vers la station que lorsque quelqu'un en a besoin, et Suspense maintient l'écran d'attente jusqu'à l'arrivée de toute la cargaison.
Code de cette leçon : App.vue
1<script setup>
2import { defineAsyncComponent, ref } from 'vue'
3import LoadingSpinner from './LoadingSpinner.vue'
4
5// Simulation de composant asynchrone (dans une vraie 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>Module lourd chargé !</h2>
13 <p>Visualisation de données complexes prête</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>Composants asynchrones & Suspense</h1>
34
35 <button @click="showModule = !showModule" class="btn">
36 {{ showModule ? 'Masquer' : 'Charger' }} le module lourd
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>Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. À quoi sert defineAsyncComponent dans Vue ?
2. Quel slot de <Suspense> affiche le contenu pendant le chargement ?
Tâches pratiques dans le jeu
- Tri vertical
Ordonne les étapes du fonctionnement de Suspense :
- Éditeur de code
Utilise defineAsyncComponent pour le chargement paresseux du composant HeavyModule. Affiche 'Chargement...' pendant le chargement.
- Tri horizontal
Ordonne le code pour définir un composant asynchrone :
- Clique dans l'ordre
Ordonne le code de Suspense avec un composant asynchrone :
- Éditeur de code
Utilise <Suspense> avec les slots #default et #fallback pour afficher un composant asynchrone avec un spinner de chargement.