ListView JavaScript Performante en 2026 : Windowing, Virtualisation et Optimisation
Maîtrisez les techniques de virtualisation et windowing pour créer des listes JavaScript ultra-performantes. Guide complet avec React et NestJS.

L'affichage de grandes quantités de données dans une application web représente un défi majeur pour les développeurs JavaScript. Qu'il s'agisse de milliers de produits dans un catalogue e-commerce ou de millions d'entrées dans un tableau de bord analytique, une approche naïve du rendu conduit inévitablement à des problèmes de performance. La virtualisation et le windowing constituent les solutions modernes pour résoudre ces problématiques.
La virtualisation de liste peut réduire le nombre d'éléments DOM de plusieurs milliers à quelques dizaines, améliorant drastiquement les temps de rendu et la consommation mémoire.
Comprendre le Problème de Performance des Listes
Lorsqu'une application JavaScript rend une liste de 10 000 éléments, chaque élément génère des nœuds DOM. Le navigateur doit calculer les styles, effectuer le layout et peindre chaque élément. Cette opération consomme de la mémoire et du temps CPU, provoquant des freezes et une expérience utilisateur dégradée.
Le problème s'aggrave avec la complexité des éléments. Un simple texte dans un <li> a un impact limité, mais une carte produit avec image, description et boutons multiplie les coûts de rendu.
// Approche naïve - À éviter avec de grandes listes
interface Product {
id: string;
name: string;
price: number;
imageUrl: string;
}
function ProductList({ products }: { products: Product[] }) {
return (
<ul>
{products.map((product) => (
<li key={product.id}>
<ProductCard product={product} />
</li>
))}
</ul>
);
}Le Principe du Windowing
Le windowing, ou fenêtrage, consiste à ne rendre que les éléments visibles dans la zone d'affichage (viewport) plus un buffer de quelques éléments au-dessus et en dessous. Les éléments hors écran sont détruits et recréés dynamiquement lors du défilement.
Cette technique réduit le nombre de nœuds DOM actifs de plusieurs ordres de grandeur. Une liste de 100 000 éléments peut ainsi fonctionner avec seulement 20 à 50 nœuds DOM visibles.
// Configuration de base pour le windowing
interface WindowConfig {
itemHeight: number;
containerHeight: number;
overscan: number; // Éléments supplémentaires rendus hors écran
}
function calculateVisibleRange(
scrollTop: number,
config: WindowConfig,
totalItems: number
): { startIndex: number; endIndex: number } {
const { itemHeight, containerHeight, overscan } = 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
);
return { startIndex, endIndex };
}Implémentation avec React Window
React Window représente la bibliothèque de référence pour la virtualisation de listes en React. Légère et performante, elle gère automatiquement le calcul des positions et le recyclage des éléments.
import { FixedSizeList as List } from 'react-window';
import { memo, useCallback } from 'react';
interface RowProps {
index: number;
style: React.CSSProperties;
data: Product[];
}
const Row = memo(function Row({ index, style, data }: RowProps) {
const product = data[index];
return (
<div style={style} className="product-row">
<img
src={product.imageUrl}
alt={product.name}
loading="lazy"
/>
<span>{product.name}</span>
<span>{product.price.toFixed(2)} EUR</span>
</div>
);
});
function VirtualizedProductList({ products }: { products: Product[] }) {
const itemData = products;
return (
<List
height={600}
itemCount={products.length}
itemSize={80}
width="100%"
itemData={itemData}
>
{Row}
</List>
);
}Listes à Hauteur Variable
Les listes réelles contiennent souvent des éléments de hauteurs différentes. React Window propose le composant VariableSizeList pour gérer cette situation.
import { VariableSizeList as List } from 'react-window';
import { useRef, useCallback } from 'react';
interface Message {
id: string;
content: string;
author: string;
}
function ChatMessageList({ messages }: { messages: Message[] }) {
const listRef = useRef<List>(null);
// Cache des hauteurs calculées
const heightCache = useRef<Map<number, number>>(new Map());
const getItemHeight = useCallback((index: number): number => {
if (heightCache.current.has(index)) {
return heightCache.current.get(index)!;
}
// Estimation basée sur la longueur du contenu
const message = messages[index];
const baseHeight = 60;
const charsPerLine = 50;
const lineHeight = 24;
const extraLines = Math.ceil(message.content.length / charsPerLine);
const height = baseHeight + (extraLines - 1) * lineHeight;
heightCache.current.set(index, height);
return height;
}, [messages]);
const handleContentResize = useCallback((index: number, height: number) => {
heightCache.current.set(index, height);
listRef.current?.resetAfterIndex(index);
}, []);
return (
<List
ref={listRef}
height={500}
itemCount={messages.length}
itemSize={getItemHeight}
width="100%"
>
{({ index, style }) => (
<MessageRow
message={messages[index]}
style={style}
onResize={(height) => handleContentResize(index, height)}
/>
)}
</List>
);
}Backend NestJS pour le Chargement Paginé
La virtualisation côté client fonctionne optimalement avec un backend qui supporte la pagination. NestJS permet d'implémenter facilement des endpoints paginés performants.
import { Controller, Get, Query } from '@nestjs/common';
import { ProductService } from './product.service';
interface PaginationQuery {
offset: number;
limit: number;
search?: string;
}
interface PaginatedResponse<T> {
data: T[];
total: number;
offset: number;
limit: number;
}
@Controller('products')
export class ProductController {
constructor(private readonly productService: ProductService) {}
@Get()
async getProducts(
@Query('offset') offset = 0,
@Query('limit') limit = 50,
@Query('search') search?: string
): Promise<PaginatedResponse<Product>> {
const [data, total] = await Promise.all([
this.productService.findMany({
skip: Number(offset),
take: Math.min(Number(limit), 100), // Limite maximale
where: search
? { name: { contains: search, mode: 'insensitive' } }
: undefined,
}),
this.productService.count({
where: search
? { name: { contains: search, mode: 'insensitive' } }
: undefined,
}),
]);
return {
data,
total,
offset: Number(offset),
limit: Number(limit),
};
}
}Infinite Scroll avec Virtualisation
L'infinite scroll combine la virtualisation avec le chargement progressif des données. Cette approche offre la meilleure expérience utilisateur pour les grandes collections.
import { FixedSizeList as List } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import { useInfiniteQuery } from '@tanstack/react-query';
function InfiniteProductList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['products'],
queryFn: async ({ pageParam = 0 }) => {
const response = await fetch(
`/api/products?offset=${pageParam}&limit=50`
);
return response.json();
},
getNextPageParam: (lastPage) => {
const nextOffset = lastPage.offset + lastPage.limit;
return nextOffset < lastPage.total ? nextOffset : undefined;
},
initialPageParam: 0,
});
const allProducts = data?.pages.flatMap((page) => page.data) ?? [];
const totalCount = data?.pages[0]?.total ?? 0;
const isItemLoaded = (index: number) => {
return !hasNextPage || index < allProducts.length;
};
const loadMoreItems = isFetchingNextPage
? () => Promise.resolve()
: () => fetchNextPage();
return (
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={hasNextPage ? allProducts.length + 1 : allProducts.length}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<List
ref={ref}
height={600}
itemCount={totalCount}
itemSize={80}
width="100%"
onItemsRendered={onItemsRendered}
>
{({ index, style }) => {
if (!isItemLoaded(index)) {
return (
<div style={style} className="loading-placeholder">
Chargement...
</div>
);
}
return (
<ProductRow
product={allProducts[index]}
style={style}
/>
);
}}
</List>
)}
</InfiniteLoader>
);
}Optimisation des Performances de Rendu
Plusieurs techniques complémentaires améliorent les performances de la virtualisation.
import { memo, useMemo, useCallback, useRef } from 'react';
import { areEqual } from 'react-window';
// Memoization agressive des composants de ligne
const ProductRow = memo(function ProductRow({
product,
style,
onSelect,
}: {
product: Product;
style: React.CSSProperties;
onSelect: (id: string) => void;
}) {
const handleClick = useCallback(() => {
onSelect(product.id);
}, [product.id, onSelect]);
return (
<div style={style} onClick={handleClick}>
<span>{product.name}</span>
<span>{product.price.toFixed(2)} EUR</span>
</div>
);
}, areEqual);
// Hook personnalisé pour la gestion du scroll optimisée
function useOptimizedScroll(
onScroll: (scrollTop: number) => void,
delay = 16
) {
const lastScrollTop = useRef(0);
const ticking = useRef(false);
return useCallback((event: React.UIEvent<HTMLDivElement>) => {
const scrollTop = event.currentTarget.scrollTop;
lastScrollTop.current = scrollTop;
if (!ticking.current) {
requestAnimationFrame(() => {
onScroll(lastScrollTop.current);
ticking.current = false;
});
ticking.current = true;
}
}, [onScroll]);
}Grilles Virtualisées
Les grilles à deux dimensions nécessitent une approche de virtualisation bidirectionnelle. React Window fournit le composant FixedSizeGrid pour ce cas d'usage.
import { FixedSizeGrid as Grid } from 'react-window';
interface GridItem {
id: string;
thumbnailUrl: string;
title: string;
}
function VirtualizedImageGrid({ items }: { items: GridItem[] }) {
const columnCount = 4;
const rowCount = Math.ceil(items.length / columnCount);
const Cell = useCallback(
({
columnIndex,
rowIndex,
style,
}: {
columnIndex: number;
rowIndex: number;
style: React.CSSProperties;
}) => {
const index = rowIndex * columnCount + columnIndex;
if (index >= items.length) {
return null;
}
const item = items[index];
return (
<div style={style} className="grid-cell">
<img
src={item.thumbnailUrl}
alt={item.title}
loading="lazy"
/>
<span>{item.title}</span>
</div>
);
},
[items, columnCount]
);
return (
<Grid
columnCount={columnCount}
columnWidth={200}
height={600}
rowCount={rowCount}
rowHeight={250}
width={800}
>
{Cell}
</Grid>
);
}Mesure des Performances
La mesure précise des performances permet de valider l'efficacité de la virtualisation et d'identifier les goulots d'étranglement.
import { useEffect, useRef } from 'react';
function useRenderMetrics(componentName: string) {
const renderCount = useRef(0);
const lastRenderTime = useRef(performance.now());
useEffect(() => {
const currentTime = performance.now();
const timeSinceLastRender = currentTime - lastRenderTime.current;
renderCount.current++;
if (process.env.NODE_ENV === 'development') {
console.log(
`[${componentName}] Render #${renderCount.current}`,
`Time since last: ${timeSinceLastRender.toFixed(2)}ms`
);
}
lastRenderTime.current = currentTime;
});
}
// Service NestJS pour le monitoring des performances
import { Injectable } from '@nestjs/common';
@Injectable()
export class PerformanceService {
private metrics: Map<string, number[]> = new Map();
recordQueryTime(queryName: string, duration: number): void {
if (!this.metrics.has(queryName)) {
this.metrics.set(queryName, []);
}
const times = this.metrics.get(queryName)!;
times.push(duration);
// Conserver uniquement les 100 dernières mesures
if (times.length > 100) {
times.shift();
}
}
getAverageQueryTime(queryName: string): number {
const times = this.metrics.get(queryName);
if (!times || times.length === 0) return 0;
return times.reduce((sum, t) => sum + t, 0) / times.length;
}
}Prêt à réussir tes entretiens Node.js / NestJS ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Conclusion
La virtualisation de listes représente une technique fondamentale pour les applications JavaScript modernes manipulant de grandes quantités de données. En combinant le windowing côté client avec une pagination efficace côté serveur, les développeurs peuvent créer des interfaces fluides capables d'afficher des millions d'éléments sans compromis sur les performances.
Les outils comme React Window simplifient considérablement l'implémentation de ces patterns. L'intégration avec TanStack Query pour l'infinite scroll et NestJS pour le backend crée une architecture complète et performante. La clé du succès réside dans la mesure continue des performances et l'optimisation itérative basée sur des données réelles.
Tu saurais repérer le bug en Node.js / NestJS ?
Un vrai bout de code, un bug caché, une tentative par jour. Sans compte pour essayer.

Écrit par
Anthony Fillion-MailletFondateur de SharpSkill
Développeur fullstack depuis plus de 10 ans. Il dirige SharpSkill et répond de tout ce qui y est publié.
Mis à jour le 15 septembre 2026
Partager
Articles similaires

NestJS et Redis en 2026 : Mise en Cache, Sessions et Questions d'Entretien
Guide complet sur l'intégration de Redis avec NestJS : configuration du cache-manager, gestion des sessions distribuées et questions techniques pour les entretiens développeur.

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.

NestJS et MongoDB en 2026 : Mongoose, Agrégations et Questions d'Entretien
Guide complet sur l'intégration de NestJS 12 avec MongoDB via Mongoose 9. Découvrez les schémas, les pipelines d'agrégation et les questions techniques fréquentes en entretien.