Aller au contenu principal

[Lv3] Implémentation du Virtual Scrolling : gestion du rendu de grandes quantités de données

Lorsqu'une page doit afficher plus de 1000 lignes de données, le Virtual Scrolling permet de réduire les nœuds DOM de 1000+ à 20-30 et l'utilisation mémoire de 80 %.


Question type en entretien

Q : Lorsqu'une page contient plusieurs tables, chacune avec plus de cent lignes de données, et que des événements de mise à jour fréquente du DOM existent, quelle méthode utiliseriez-vous pour optimiser les performances de cette page ?


Analyse du problème (Situation)

Scénario réel du projet

Dans un projet de plateforme, certaines pages doivent traiter de grandes quantités de données :

📊 Page d'historique
├─ Table des dépôts : 1000+ lignes
├─ Table des retraits : 800+ lignes
├─ Table des mises : 5000+ lignes
└─ Chaque ligne comporte 8 à 10 colonnes (date, montant, statut, etc.)

❌ Problèmes sans optimisation
├─ Nombre de nœuds DOM : 1000 lignes × 10 colonnes = 10 000+ nœuds
├─ Consommation mémoire : environ 150-200 Mo
├─ Temps du premier rendu : 3-5 secondes (écran blanc)
├─ Saccades au défilement : FPS < 20
└─ Lors des mises à jour WebSocket : la table entière est re-rendue (très lent)

Gravité du problème

// ❌ Approche traditionnelle
<tr v-for="record in allRecords"> // 1000+ lignes toutes rendues
<td>{{ record.time }}</td>
<td>{{ record.amount }}</td>
// ... 8 à 10 colonnes
</tr>

// Résultat :
// - Rendu initial : 10 000+ nœuds DOM
// - Réellement visible pour l'utilisateur : 20-30 lignes
// - Gaspillage : 99 % des nœuds sont invisibles pour l'utilisateur

Solution (Action)

Virtual Scrolling

Concernant l'optimisation du Virtual Scrolling, deux directions principales : la première est d'utiliser une librairie tierce officiellement recommandée comme vue-virtual-scroller, qui détermine les lignes visibles en fonction des paramètres et des besoins.

// Ne rendre que les lignes visibles, par exemple :
// - 100 lignes de données, seules les 20 visibles sont rendues
// - Réduction considérable du nombre de nœuds DOM

L'autre option est de l'implémenter soi-même, mais compte tenu du coût de développement réel et de la couverture des cas d'utilisation, je pencherais davantage pour la librairie tierce recommandée.

Contrôle de la fréquence de mise à jour des données

Solution 1 : requestAnimationFrame (RAF) Concept : le navigateur ne peut rafraîchir que 60 fois par seconde maximum (60 FPS). Les mises à jour plus rapides sont invisibles à l'œil, donc on synchronise avec le taux de rafraîchissement de l'écran.

// ❌ Avant : mise à jour immédiate à chaque réception (potentiellement 100 fois/seconde)
socket.on('price', (newPrice) => {
btcPrice.value = newPrice;
});

// ✅ Amélioré : collecter les données, mise à jour synchronisée avec le rafraîchissement (max 60 fois/seconde)
let latestPrice = null;
let isScheduled = false;

socket.on('price', (newPrice) => {
latestPrice = newPrice; // Stocker le dernier prix

if (!isScheduled) {
isScheduled = true;
requestAnimationFrame(() => {
btcPrice.value = latestPrice; // Mise à jour au moment du rafraîchissement du navigateur
isScheduled = false;
});
}
});

Solution 2 : Throttle Concept : limiter de force la fréquence des mises à jour, par exemple "maximum 1 mise à jour par 100 ms"

// throttle de lodash (si déjà utilisé dans le projet)
import { throttle } from 'lodash-es';

const updatePrice = throttle((newPrice) => {
btcPrice.value = newPrice;
}, 100); // Maximum 1 exécution par 100 ms

socket.on('price', updatePrice);

Optimisations spécifiques à Vue 3

Certains sucres syntaxiques de Vue 3 offrent des optimisations de performance, comme v-memo, bien que personnellement j'utilise rarement ce cas.

// 1. v-memo - mémoriser les colonnes qui changent rarement
<tr v-for="row in data"
:key="row.id"
v-memo="[row.price, row.volume]"> // Re-rendu uniquement quand ces champs changent
</tr>

// 2. Geler les données statiques pour éviter le surcoût du système réactif
const staticData = Object.freeze(largeDataArray)

// 3. shallowRef pour les grands tableaux
const tableData = shallowRef([...]) // Ne suit que le tableau lui-même, pas les objets internes

// 4. Utiliser key pour optimiser l'algorithme de diff (un id unique par item pour limiter les mises à jour DOM aux nœuds modifiés)
<tr v-for="row in data" :key="row.id"> // Key stable**

RAF : synchronisé avec le rafraîchissement écran (~16 ms), adapté aux animations et au défilement Throttle : intervalle personnalisé (ex. 100 ms), adapté à la recherche et au resize

Optimisation du rendu DOM

// Utiliser CSS transform au lieu de top/left
.row-update {
transform: translateY(0); /* Déclenche l'accélération GPU */
will-change: transform; /* Indique au navigateur d'optimiser */
}

// CSS containment pour isoler la zone de rendu
.table-container {
contain: layout style paint;
}

Résultats de l'optimisation (Result)

Comparaison des performances

IndicateurAvant optimisationAprès optimisationAmélioration
Nœuds DOM10 000+20-30↓ 99,7 %
Utilisation mémoire150-200 Mo30-40 Mo↓ 80 %
Premier rendu3-5 s0,3-0,5 s↑ 90 %
FPS défilement< 2055-60↑ 200 %
Réponse aux mises à jour500-800 ms16-33 ms↑ 95 %

Résultats concrets

✅ Virtual Scrolling
├─ Seules les 20-30 lignes visibles sont rendues
├─ Mise à jour dynamique de la zone visible pendant le défilement
├─ Imperceptible pour l'utilisateur (expérience fluide)
└─ Mémoire stable (ne croît pas avec le volume de données)

✅ Mise à jour des données via RAF
├─ WebSocket : 100 mises à jour/seconde → maximum 60 rendus
├─ Synchronisé avec le taux de rafraîchissement (60 FPS)
└─ Utilisation CPU réduite de 60 %

✅ Optimisations Vue 3
├─ v-memo : évite les re-rendus inutiles
├─ shallowRef : réduit le surcoût réactif
└─ :key stable : optimise l'algorithme de diff

Points clés pour l'entretien

Questions d'approfondissement courantes

Q : Et si on ne peut pas utiliser de librairie tierce ? R : Implémenter la logique fondamentale du Virtual Scrolling soi-même :

// Concept fondamental
const itemHeight = 50; // Hauteur de chaque ligne
const containerHeight = 600; // Hauteur du conteneur
const visibleCount = Math.ceil(containerHeight / itemHeight); // Nombre visible

// Calculer quels éléments doivent être affichés
const scrollTop = container.scrollTop;
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = startIndex + visibleCount;

// Ne rendre que la zone visible
const visibleItems = allItems.slice(startIndex, endIndex);

// Compenser la hauteur avec du padding (pour une barre de défilement correcte)
const paddingTop = startIndex * itemHeight;
const paddingBottom = (allItems.length - endIndex) * itemHeight;

Points clés :

  • Calcul de la zone visible (startIndex -> endIndex)
  • Chargement dynamique des données (slice)
  • Compensation de la hauteur (padding top/bottom)
  • Écoute de l'événement scroll (optimisation throttle)

Q : Comment gérer la reconnexion après une déconnexion WebSocket ? R : Implémenter une stratégie de reconnexion avec backoff exponentiel :

let retryCount = 0;
const maxRetries = 5;
const baseDelay = 1000; // 1 seconde

function reconnect() {
if (retryCount >= maxRetries) {
showError('Connexion impossible, veuillez rafraîchir la page');
return;
}

// Backoff exponentiel : 1s → 2s → 4s → 8s → 16s
const delay = baseDelay * Math.pow(2, retryCount);

setTimeout(() => {
retryCount++;
connectWebSocket();
}, delay);
}

// Après une reconnexion réussie
socket.on('connect', () => {
retryCount = 0; // Réinitialiser le compteur
syncData(); // Synchroniser les données
showSuccess('Connexion rétablie');
});

Q : Comment tester les effets de l'optimisation des performances ? R : En combinant plusieurs outils :

// 1. Mesure du FPS avec la Performance API
let lastTime = performance.now();
let frames = 0;

function measureFPS() {
frames++;
const currentTime = performance.now();
if (currentTime >= lastTime + 1000) {
console.log(`FPS: ${frames}`);
frames = 0;
lastTime = currentTime;
}
requestAnimationFrame(measureFPS);
}

// 2. Memory Profiling (Chrome DevTools)
// - Snapshot avant le rendu
// - Snapshot après le rendu
// - Comparer la différence de mémoire

// 3. Lighthouse / Performance Tab
// - Temps des Long Tasks
// - Total Blocking Time
// - Cumulative Layout Shift

// 4. Tests automatisés (Playwright)
const { test } = require('@playwright/test');

test('virtual scroll performance', async ({ page }) => {
await page.goto('/records');

// Mesurer le temps du premier rendu
const renderTime = await page.evaluate(() => {
const start = performance.now();
// Déclencher le rendu
const end = performance.now();
return end - start;
});

expect(renderTime).toBeLessThan(500); // < 500ms
});

Q : Quels sont les inconvénients du Virtual Scroll ? R : Des compromis à noter :

❌ Inconvénients
├─ Impossible d'utiliser la recherche native du navigateur (Ctrl+F)
├─ La fonction "tout sélectionner" nécessite un traitement spécial
├─ Complexité d'implémentation élevée
├─ Nécessite une hauteur fixe ou un calcul préalable de la hauteur
└─ L'accessibilité nécessite un traitement supplémentaire

✅ Cas adaptés
├─ Volume de données > 100 lignes
├─ Structure de données similaire (hauteur fixe)
├─ Défilement haute performance requis
└─ Consultation principalement (pas d'édition)

❌ Cas non adaptés
├─ Volume < 50 lignes (sur-ingénierie)
├─ Hauteur variable (implémentation difficile)
├─ Beaucoup d'interactions (multi-sélection, drag & drop)
└─ Besoin d'imprimer la table entière

Q : Comment optimiser une liste à hauteur variable ? R : Utiliser le Virtual Scrolling à hauteur dynamique :

// Solution 1 : hauteur estimée + mesure réelle
const estimatedHeight = 50; // Hauteur estimée
const measuredHeights = {}; // Hauteurs réelles enregistrées

// Mesurer après le rendu
onMounted(() => {
const elements = document.querySelectorAll('.list-item');
elements.forEach((el, index) => {
measuredHeights[index] = el.offsetHeight;
});
});

// Solution 2 : utiliser une librairie supportant la hauteur dynamique
// vue-virtual-scroller supporte le dynamic-height
<DynamicScroller
:items="items"
:min-item-size="50" // Hauteur minimale
:buffer="200" // Zone tampon
/>

Comparaison technique

Virtual Scroll vs Pagination

CritèreVirtual ScrollPagination traditionnelle
Expérience utilisateurDéfilement continu (meilleur)Changement de page nécessaire (interrompu)
PerformanceToujours uniquement la zone visibleRendu complet de chaque page
Difficulté d'implémentationPlus complexeSimple
SEOMoins bonMeilleur
AccessibilitéTraitement spécial nécessaireSupport natif

Recommandation :

  • Back-office, Dashboard -> Virtual Scroll
  • Site public, blog -> Pagination traditionnelle
  • Solution hybride : Virtual Scroll + bouton "Charger plus"