frontend / vuejs
Performance — lazy loading, keep-alive, v-memo
Explication
Ce que vous allez apprendre
- Découper le bundle JavaScript par route et par composant lourd pour réduire le chargement initial
- Préserver l'état de composants inactifs avec
<KeepAlive>plutôt que de les recréer - Éviter le re-rendu d'un sous-arbre stable dans une grande liste avec
v-memo - Comprendre pourquoi une liste de plusieurs milliers d'éléments nécessite la virtualisation
- Surveiller activement la taille des bundles de production
Dans quel contexte ?
Une application grossit au fil des mois : de nouvelles pages, un éditeur de texte riche rarement utilisé, un tableau de statistiques affichant 10 000 lignes. Sans optimisation, le bundle JavaScript initial finit par peser plusieurs mégaoctets même pour un visiteur qui ne consultera jamais l'éditeur riche, et le tableau de 10 000 lignes rendrait le navigateur visiblement poussif. Cette leçon rassemble les techniques pour éviter ces deux écueils classiques.
D'abord, ne charger que ce dont la page a vraiment besoin
Le code splitting par route (component: () => import('./pages/Admin.vue')) transforme chaque page en un fragment de code séparé, téléchargé uniquement quand cette page est visitée. Un visiteur normal qui ne visite jamais /admin ne téléchargera jamais le code de cette page — un gain direct sur le temps de chargement initial perçu.
Prérequis
Il faut avoir compris le routing (leçons 12-13) et <Suspense>/defineAsyncComponent (leçon 16) : cette leçon les combine dans une optique explicitement orientée performance.
Ensuite, la même logique appliquée à des composants isolés, pas seulement des pages
defineAsyncComponent applique le même principe à des composants ponctuels particulièrement lourds (un éditeur riche, une bibliothèque de graphiques), même à l'intérieur d'une page déjà chargée. Ces composants ne pèsent sur le bundle QUE lorsqu'ils sont effectivement affichés (v-if="modeEdition").
| Technique | Réduit quoi | Quand l'utiliser |
|---|---|---|
| Lazy loading de route | Bundle initial global | Systématiquement, dès que l'app a plusieurs pages |
defineAsyncComponent | Bundle d'une page précise | Composant lourd rarement affiché |
<KeepAlive> | Coût de recréation d'un composant | Onglets, formulaires à préserver |
v-memo | Re-rendus inutiles dans une liste | Grande liste avec des lignes stables |
Il reste un coût différent : celui de recréer un composant à chaque affichage
Sans <KeepAlive>, changer d'onglet détruit puis recrée entièrement le composant à chaque bascule, perdant tout état interne (position de scroll, saisie en cours). <KeepAlive :max="5"> conserve les composants en cache jusqu'à une limite définie, évitant à la fois la recréation coûteuse et une consommation mémoire illimitée.
Maintenant, réduire les re-rendus inutiles au sein d'une liste
v-memo="[condition]" mémorise un sous-arbre du template : il n'est re-rendu que si l'une des valeurs surveillées change réellement. Sur une grande liste où chaque ligne contient un sous-composant coûteux, ça évite de tout recalculer à chaque frappe si la sélection ne change pas pour la majorité des lignes.
Piège fréquent
Utiliser un simple v-for sur 10 000 éléments crée 10 000 nœuds DOM réels, même si seule une fraction est visible à l'écran — une catastrophe de performance quel que soit le soin apporté au reste du code. Au-delà de quelques centaines de lignes visibles simultanément, la virtualisation (ne rendre que les lignes réellement visibles, comme avec @tanstack/vue-virtual) devient incontournable.
Astuce
rollup-plugin-visualizer génère une carte visuelle du contenu du bundle de production, révélant souvent des dépendances inattendues qui pèsent bien plus lourd que prévu. Un contrôle à intégrer régulièrement, pas seulement une fois en fin de projet.
Maintenant que la performance est maîtrisée, la prochaine leçon aborde un renfort transversal à tout ce qui a été vu jusqu'ici : l'intégration de TypeScript pour sécuriser le code à la compilation.
Commandes & code
Performance — lazy loading, keep-alive, v-memo
Optimiser une application Vue à grande échelle : découpage du bundle, mise en cache, réduction des re-rendus.
// 1. Code splitting par route -- chaque page devient un chunk séparé, chargé à la demande
const routes = [
{ path: '/', component: () => import('./pages/Accueil.vue') },
{ path: '/admin', component: () => import('./pages/Admin.vue') }, // jamais téléchargé pour un visiteur normal
]<!-- 2. defineAsyncComponent pour des composants lourds hors du chemin critique -->
<script setup>
import { defineAsyncComponent } from 'vue'
const EditeurRiche = defineAsyncComponent(() => import('./components/EditeurRiche.vue'))
const GraphiqueLourd = defineAsyncComponent(() => import('./components/GraphiqueLourd.vue'))
</script>
<template>
<!-- ces gros composants (souvent liés à des libs tierces volumineuses) ne pèsent
sur le bundle initial QUE lorsqu'ils sont effectivement affichés -->
<EditeurRiche v-if="modeEdition" />
<GraphiqueLourd v-if="ongletActif === 'stats'" />
</template><!-- 3. KeepAlive : évite de détruire/recréer des composants coûteux au changement d'onglet -->
<script setup>
import { ref } from 'vue'
const ongletActif = ref('profil')
</script>
<template>
<!-- include/exclude limitent le cache aux composants nommés voulus (par leur `name`) -->
<KeepAlive :include="['OngletProfil', 'OngletStatistiques']" :max="5">
<component :is="ongletActif === 'profil' ? OngletProfil : OngletStatistiques" />
</KeepAlive>
</template><!-- 4. v-memo : "memoïse" un sous-arbre, il n'est re-rendu QUE si une des valeurs surveillées change -->
<template>
<div v-for="item in listeVolumineuse" :key="item.id" v-memo="[item.id === itemSelectionne]">
<!-- sous-arbre coûteux : ne se re-rend PAS à chaque frappe si la sélection ne change pas pour CET item -->
<GraphiqueMiniature :donnees="item.historique" />
<span :class="{ actif: item.id === itemSelectionne }">{{ item.nom }}</span>
</div>
</template>// 5. Virtualisation de longues listes : ne rendre QUE les éléments visibles à l'écran
// (v-for seul sur 10 000 lignes crée 10 000 noeuds DOM -> catastrophe de performance)
// npm install @tanstack/vue-virtual
import { useVirtualizer } from '@tanstack/vue-virtual'
import { ref, computed } from 'vue'
const conteneurRef = ref(null)
const items = ref(Array.from({ length: 10000 }, (_, i) => `Ligne ${i}`))
const virtualiseur = useVirtualizer(computed(() => ({
count: items.value.length,
getScrollElement: () => conteneurRef.value,
estimateSize: () => 40, // hauteur estimée d'une ligne en px
overscan: 5, // marge de lignes rendues hors-écran pour un scroll fluide
})))// 6. shallowRef pour de grosses structures peu mutées en profondeur (évite le proxy récursif coûteux)
import { shallowRef } from 'vue'
const grosJeuDeDonnees = shallowRef(chargerDonneesVolumineuses())
// 7. Éviter les computed inutilement recréés à chaque rendu à l'intérieur d'un v-for
// Mauvais : une fonction inline recalculée à chaque évaluation du template
// <div v-for="x in liste">{{ liste.filter(y => y.cat === x.cat).length }}</div>
// Bon : précalculer en amont dans une computed unique
const compteursParCategorie = computed(() => {
const compteurs = {}
for (const x of liste.value) compteurs[x.cat] = (compteurs[x.cat] ?? 0) + 1
return compteurs
})# 8. Analyser le bundle de production pour traquer les dépendances lourdes
npm install -D rollup-plugin-visualizer
# puis dans vite.config.js : plugins: [vue(), visualizer({ open: true })]
npm run buildRésumé
- Découper par route et par composant lourd (
() => import(...)) : réduit drastiquement le bundle initial. <KeepAlive>conserve l'état des composants inactifs plutôt que de les recréer.v-memoévite de re-rendre un sous-arbre stable dans une grande liste.- Pour des listes de milliers d'éléments, la virtualisation est incontournable (le DOM reste le goulot d'étranglement).
Exercices pratiques
Mission : un tableau de statistiques qui fige le navigateur
Objectif : Diagnostiquer un rendu non virtualisé de 10 000 lignes et un re-rendu inutile par ligne, puis proposer des corrections adaptées.
Contexte
Un tableau de statistiques affiche 10 000 lignes avec un simple v-for="item in items" :key="item.id", chaque ligne contenant un <GraphiqueMiniature :donnees="item.historique" /> coûteux à rendre. Chaque frappe dans un champ de recherche situé au-dessus du tableau (qui ne filtre qu'un compteur affiché à part, sans toucher items) fait ramer visiblement toute la page pendant une fraction de seconde.