# ListView JavaScript Haute Performance en 2026 : Virtualisation et Techniques d'Optimisation > Guide complet sur l'implémentation de listes virtualisées haute performance en JavaScript. Découvrez les techniques de virtualisation, l'optimisation du rendu et les meilleures pratiques pour gérer des milliers d'éléments sans ralentissement. - Published: 2026-09-07 - Updated: 2026-09-07 - Author: Anthony Fillion-Maillet - Tags: javascript, performance, virtualisation, react, nestjs - Reading time: 12 min --- La gestion efficace de grandes listes de données représente l'un des défis les plus courants du développement frontend moderne. Lorsqu'une application doit afficher des milliers, voire des millions d'éléments, les approches traditionnelles de rendu atteignent rapidement leurs limites, provoquant des ralentissements significatifs et une expérience utilisateur dégradée. La virtualisation de listes constitue la solution de référence pour résoudre ce problème. Cette technique consiste à ne rendre que les éléments visibles dans la zone d'affichage (viewport), tout en simulant la présence de l'ensemble des données. Le navigateur ne manipule ainsi qu'une fraction du DOM, réduisant drastiquement la charge mémoire et le temps de rendu. > La virtualisation peut réduire le nombre d'éléments DOM de plusieurs milliers à quelques dizaines, améliorant les performances de rendu de plus de 90% dans certains cas. ## Comprendre les Fondamentaux de la Virtualisation Le principe de la virtualisation repose sur un concept simple : pourquoi rendre 10 000 éléments si l'utilisateur n'en voit que 20 à la fois ? La virtualisation calcule dynamiquement quels éléments doivent être visibles en fonction de la position de défilement, puis ne rend que ces éléments spécifiques. Cette approche nécessite plusieurs calculs essentiels : - La hauteur totale estimée de la liste complète - L'index du premier élément visible - L'index du dernier élément visible - Le décalage (offset) pour positionner correctement les éléments ```typescript interface VirtualizedListConfig { itemHeight: number; containerHeight: number; overscan: number; totalItems: number; } function calculateVisibleRange( scrollTop: number, config: VirtualizedListConfig ): { startIndex: number; endIndex: number; offsetY: number } { const { itemHeight, containerHeight, overscan, totalItems } = config; const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan); const visibleCount = Math.ceil(containerHeight / itemHeight); const endIndex = Math.min(totalItems - 1, startIndex + visibleCount + overscan * 2); const offsetY = startIndex * itemHeight; return { startIndex, endIndex, offsetY }; } ``` ## Implémentation d'une Liste Virtualisée avec NestJS et WebSocket Dans le contexte d'une application full-stack, la virtualisation côté client doit souvent s'accompagner d'une gestion intelligente des données côté serveur. NestJS offre une architecture robuste pour implémenter ce type de fonctionnalité. ```typescript import { Injectable } from '@nestjs/common'; import { WebSocketGateway, WebSocketServer, SubscribeMessage } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; interface PaginatedRequest { startIndex: number; endIndex: number; searchQuery?: string; } @Injectable() @WebSocketGateway({ cors: true }) export class VirtualizedListGateway { @WebSocketServer() server: Server; private dataCache: Map = new Map(); @SubscribeMessage('requestItems') async handleItemRequest( client: Socket, payload: PaginatedRequest ): Promise { const { startIndex, endIndex, searchQuery } = payload; const cacheKey = `${searchQuery || 'all'}_${startIndex}_${endIndex}`; if (this.dataCache.has(cacheKey)) { client.emit('itemsResponse', { items: this.dataCache.get(cacheKey), startIndex, endIndex, cached: true }); return; } const items = await this.fetchItemsFromDatabase(startIndex, endIndex, searchQuery); this.dataCache.set(cacheKey, items); client.emit('itemsResponse', { items, startIndex, endIndex, cached: false }); } private async fetchItemsFromDatabase( start: number, end: number, query?: string ): Promise { // Implémentation de la requête base de données return []; } } ``` ## Optimisation du Rendu avec React et useDeferredValue React 19 introduit des améliorations significatives pour la gestion des mises à jour de priorité. Le hook `useDeferredValue` permet de différer les mises à jour moins critiques, évitant les blocages de l'interface lors du défilement rapide. ```typescript import React, { useDeferredValue, useMemo, useCallback, useRef, useEffect, useState } from 'react'; interface VirtualizedListProps { items: T[]; itemHeight: number; containerHeight: number; renderItem: (item: T, index: number) => React.ReactNode; overscan?: number; } function VirtualizedList({ items, itemHeight, containerHeight, renderItem, overscan = 3 }: VirtualizedListProps) { const [scrollTop, setScrollTop] = useState(0); const deferredScrollTop = useDeferredValue(scrollTop); const containerRef = useRef(null); const visibleRange = useMemo(() => { const startIndex = Math.max(0, Math.floor(deferredScrollTop / itemHeight) - overscan); const visibleCount = Math.ceil(containerHeight / itemHeight); const endIndex = Math.min(items.length - 1, startIndex + visibleCount + overscan * 2); return { startIndex, endIndex }; }, [deferredScrollTop, itemHeight, containerHeight, items.length, overscan]); const handleScroll = useCallback((e: React.UIEvent) => { setScrollTop(e.currentTarget.scrollTop); }, []); const visibleItems = useMemo(() => { return items.slice(visibleRange.startIndex, visibleRange.endIndex + 1); }, [items, visibleRange]); const totalHeight = items.length * itemHeight; const offsetY = visibleRange.startIndex * itemHeight; return (
{visibleItems.map((item, index) => renderItem(item, visibleRange.startIndex + index) )}
); } ``` ## Gestion des Hauteurs Variables Les listes avec des éléments de hauteurs différentes représentent un défi supplémentaire. Une approche efficace consiste à mesurer les hauteurs réelles des éléments et à maintenir un cache de ces mesures. ```typescript interface HeightCache { heights: Map; estimatedHeight: number; totalHeight: number; } class DynamicHeightManager { private cache: HeightCache = { heights: new Map(), estimatedHeight: 50, totalHeight: 0 }; constructor(private totalItems: number, estimatedHeight: number = 50) { this.cache.estimatedHeight = estimatedHeight; this.recalculateTotalHeight(); } setItemHeight(index: number, height: number): void { const previousHeight = this.cache.heights.get(index) || this.cache.estimatedHeight; this.cache.heights.set(index, height); this.cache.totalHeight += height - previousHeight; } getItemOffset(index: number): number { let offset = 0; for (let i = 0; i < index; i++) { offset += this.cache.heights.get(i) || this.cache.estimatedHeight; } return offset; } getItemHeight(index: number): number { return this.cache.heights.get(index) || this.cache.estimatedHeight; } getTotalHeight(): number { return this.cache.totalHeight; } findIndexAtOffset(offset: number): number { let currentOffset = 0; for (let i = 0; i < this.totalItems; i++) { const height = this.getItemHeight(i); if (currentOffset + height > offset) { return i; } currentOffset += height; } return this.totalItems - 1; } private recalculateTotalHeight(): void { this.cache.totalHeight = this.totalItems * this.cache.estimatedHeight; } } ``` ## Stratégies de Préchargement et Mise en Cache Pour garantir une expérience utilisateur fluide, le préchargement des données avant qu'elles ne soient visibles s'avère essentiel. Cette technique anticipe les besoins de l'utilisateur en chargeant les données à l'avance. ```typescript import { Injectable } from '@nestjs/common'; interface CacheEntry { data: T[]; timestamp: number; ttl: number; } @Injectable() export class PrefetchService { private cache: Map> = new Map(); private readonly DEFAULT_TTL = 60000; // 1 minute async prefetchRange( fetcher: (start: number, end: number) => Promise, currentIndex: number, bufferSize: number = 50 ): Promise { const prefetchStart = Math.max(0, currentIndex - bufferSize); const prefetchEnd = currentIndex + bufferSize; const cacheKey = `${prefetchStart}_${prefetchEnd}`; if (this.isValid(cacheKey)) { return; } const data = await fetcher(prefetchStart, prefetchEnd); this.cache.set(cacheKey, { data, timestamp: Date.now(), ttl: this.DEFAULT_TTL }); } get(start: number, end: number): T[] | null { const cacheKey = `${start}_${end}`; const entry = this.cache.get(cacheKey); if (entry && this.isValid(cacheKey)) { return entry.data as T[]; } return null; } private isValid(key: string): boolean { const entry = this.cache.get(key); if (!entry) return false; return Date.now() - entry.timestamp < entry.ttl; } clearExpired(): void { const now = Date.now(); for (const [key, entry] of this.cache.entries()) { if (now - entry.timestamp >= entry.ttl) { this.cache.delete(key); } } } } ``` ## Optimisation des Performances avec Web Workers Les calculs intensifs liés à la virtualisation peuvent être déplacés vers des Web Workers pour libérer le thread principal et maintenir une interface réactive. ```typescript // virtualization.worker.ts self.onmessage = function(e: MessageEvent) { const { type, payload } = e.data; switch (type) { case 'CALCULATE_VISIBLE_RANGE': const result = calculateRange(payload); self.postMessage({ type: 'RANGE_CALCULATED', result }); break; case 'FILTER_ITEMS': const filtered = filterItems(payload.items, payload.query); self.postMessage({ type: 'ITEMS_FILTERED', result: filtered }); break; case 'SORT_ITEMS': const sorted = sortItems(payload.items, payload.comparator); self.postMessage({ type: 'ITEMS_SORTED', result: sorted }); break; } }; function calculateRange(payload: { scrollTop: number; itemHeight: number; containerHeight: number; totalItems: number; overscan: number; }) { const { scrollTop, itemHeight, containerHeight, totalItems, overscan } = payload; const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan); const visibleCount = Math.ceil(containerHeight / itemHeight); const endIndex = Math.min(totalItems - 1, startIndex + visibleCount + overscan * 2); return { startIndex, endIndex, offsetY: startIndex * itemHeight }; } function filterItems(items: T[], query: string): T[] { return items.filter(item => JSON.stringify(item).toLowerCase().includes(query.toLowerCase()) ); } function sortItems(items: T[], comparatorKey: keyof T): T[] { return [...items].sort((a, b) => { if (a[comparatorKey] < b[comparatorKey]) return -1; if (a[comparatorKey] > b[comparatorKey]) return 1; return 0; }); } ``` ## Intégration avec les Bibliothèques Existantes Plusieurs bibliothèques éprouvées facilitent l'implémentation de la virtualisation. React Virtualized et TanStack Virtual offrent des solutions robustes et bien documentées. ```typescript import { useVirtualizer } from '@tanstack/react-virtual'; import { useRef } from 'react'; interface Item { id: string; title: string; description: string; } function TanStackVirtualList({ items }: { items: Item[] }) { const parentRef = useRef(null); const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, overscan: 5, }); return (
{virtualizer.getVirtualItems().map((virtualItem) => (

{items[virtualItem.index].title}

{items[virtualItem.index].description}

))}
); } ``` ## Mesure et Monitoring des Performances La mesure continue des performances permet d'identifier les goulots d'étranglement et d'optimiser en conséquence. L'API Performance du navigateur offre des outils précieux pour ce monitoring. ```typescript class PerformanceMonitor { private marks: Map = new Map(); private measurements: Map = new Map(); startMeasure(name: string): void { this.marks.set(name, performance.now()); } endMeasure(name: string): number { const start = this.marks.get(name); if (!start) return 0; const duration = performance.now() - start; if (!this.measurements.has(name)) { this.measurements.set(name, []); } this.measurements.get(name)!.push(duration); return duration; } getAverageTime(name: string): number { const times = this.measurements.get(name); if (!times || times.length === 0) return 0; return times.reduce((a, b) => a + b, 0) / times.length; } getReport(): Record { const report: Record = {}; for (const [name, times] of this.measurements.entries()) { report[name] = { average: this.getAverageTime(name), min: Math.min(...times), max: Math.max(...times), count: times.length }; } return report; } reset(): void { this.marks.clear(); this.measurements.clear(); } } ``` ## Conclusion La virtualisation de listes représente une technique indispensable pour tout développeur travaillant avec de grandes quantités de données. Les approches présentées dans cet article, de l'implémentation basique à l'utilisation de Web Workers, offrent un éventail de solutions adaptées à différents contextes. Les points clés à retenir incluent l'importance du calcul précis des éléments visibles, la gestion intelligente du cache pour le préchargement, et l'utilisation judicieuse des fonctionnalités de React 19 comme `useDeferredValue`. L'intégration avec des bibliothèques éprouvées comme TanStack Virtual simplifie considérablement l'implémentation tout en garantissant des performances optimales. La maîtrise de ces techniques de virtualisation constitue un atout majeur pour les entretiens techniques, où la capacité à optimiser les performances d'une application est fréquemment évaluée. Ces compétences démontrent une compréhension approfondie des mécanismes de rendu et de la gestion mémoire dans les applications JavaScript modernes. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/node-nestjs/high-performance-javascript-listview-virtualization