Retour au cours

frontend / vuejs

Performance — lazy loading, keep-alive, v-memo

Leçon 191 exercice

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").

TechniqueRéduit quoiQuand l'utiliser
Lazy loading de routeBundle initial globalSystématiquement, dès que l'app a plusieurs pages
defineAsyncComponentBundle d'une page préciseComposant lourd rarement affiché
<KeepAlive>Coût de recréation d'un composantOnglets, formulaires à préserver
v-memoRe-rendus inutiles dans une listeGrande 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.

js
// 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
]
vue
<!-- 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>
vue
<!-- 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>
vue
<!-- 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>
js
// 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
})))
js
// 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
})
bash
# 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 build

Ré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

1 disponible
1

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.

Résoudre l’exercice →