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"