Rendu Performant de Listes Massives avec React et react-window

Le Défi du Randu de Grandes Quantités de Données

Dans le développement d'applications web modernes, l'affichage de milliers, voire de centaines de milliers d'enregistrements est un défi courant. L'apporche naïve consistant à injecter l'intégralité des données dans le DOM entraîne rapidement des goulets d'étranglement sévères :

  • Surcharge du DOM : Des milliers de nœuds DOM saturent la mémoire et ralentissent le parsing initial.
  • Coût de calcul élevé : Le moteur de rendu du navigateur passe un temps considérable à calculer les styles, les mises en page (layout) et les peintures (paint).
  • Saccades au défilement : Les opérations de reflow et repaint lors du scroll détruisent la fluidité de l'interface.

Pour contourner ces limitations, la technique du défilement virtuel (ou windowing) s'impose comme la solution standard. Elle repose sur un principe simple : ne rendre à l'écran que les éléments strictement visibles dans la zone d'affichage (viewport), tout en simulant la hauteur totale du conteneur pour préserver le comportement natif de la barre de défilement.

Architecture de react-window

La bibliothèque react-window implémente cette logique de manière minimaliste et performante. Elle propose plusieurs composants adaptés à différentes topologies de données :

  • FixedSizeList : Pour les collections où chaque élément possède une dimension constante.
  • VariableSizeList : Pour les éléments dont la hauteur ou la largeur varie dynamiquement.
  • FixedSizeGrid / VariableSizeGrid : Pour les structures tabulaires ou en grille bidimensionnelle.

Mise en Œuvre : Installation et Premier Composant

Intégrez la dépendance via votre gestionnaire de paquets :

npm install react-window
# ou
yarn add react-window

Voici une implémentation fondamentale utilisant FixedSizeList pour afficher un large volume d'éléments de taille uniforme :

import { FixedSizeList } from 'react-window';

const ROW_HEIGHT = 45;
const CONTAINER_HEIGHT = 600;

function UniformVirtualList({ records }) {
  const renderRow = ({ index, style }) => (
    <div style={style} className="virtual-row">
      <span>Identifiant : {records[index].id}</span>
      <span>{records[index].label}</span>
    </div>
  );

  return (
    <FixedSizeList
      height={CONTAINER_HEIGHT}
      itemCount={records.length}
      itemSize={ROW_HEIGHT}
      width="100%"
    >
      {renderRow}
    </FixedSizeList>
  );
}

Synergie avec les React Hooks

L'association de react-window avec les Hooks de React permet de bâtir des architectures réactives et optimisées.

Gestion Asynchrone et État Local

Lorsque les données proviennent d'une API, il est crucial de gérer les états de chargement sans bloquer le thread principal :

import { useState, useEffect } from 'react';
import { FixedSizeList } from 'react-window';

function AsyncDataGrid() {
  const [entries, setEntries] = useState([]);
  const [isFetching, setIsFetching] = useState(true);

  useEffect(() => {
    const retrieveData = async () => {
      try {
        // Simulation d'une requête réseau massive
        const response = await fetchLargeDataset();
        setEntries(response);
      } catch (error) {
        console.error("Échec du chargement", error);
      } finally {
        setIsFetching(false);
      }
    };
    retrieveData();
  }, []);

  if (isFetching) return <div className="spinner">Chargement des données...</div>;

  return (
    <FixedSizeList
      height={500}
      itemCount={entries.length}
      itemSize={55}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style} className="grid-row">
          {entries[index].name}
        </div>
      )}
    </FixedSizeList>
  );
}

Mémorisation avec useCallback

Pour éviter la recréation de la fonction de rendu à chaque cycle de rendu du composant parent, encapsulez-la dans useCallback :

import { useCallback } from 'react';

// À l'intérieur du composant
const rowRenderer = useCallback(({ index, style }) => (
  <div style={style} className="memoized-row">
    {entries[index].description}
  </div>
), [entries]);

// Utilisation dans le JSX
<FixedSizeList ...>
  {rowRenderer}
</FixedSizeList>

Scénarios Avancés : Hauteurs Variables et Grilles

Listes à Dimensions Dynamiques

Si le contenu de vos lignes peut s'étendre (par exemple, du texte sur plusieurs lignes), utilisez VariableSizeList. Vous devrez fournir une fonction qui calcule ou estime la hauteur de chaque index :

import { VariableSizeList } from 'react-window';

function DynamicList({ items }) {
  // Fonction déterminant la hauteur de chaque élément
  const getItemSize = (index) => {
    const item = items[index];
    return item.isExpanded ? 120 : 60;
  };

  return (
    <VariableSizeList
      height={400}
      itemCount={items.length}
      itemSize={getItemSize}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>{items[index].content}</div>
      )}
    </VariableSizeList>
  );
}

Structures en Grille (Tableaux)

Pour les tableaux de données complexes, le composant Grid virtualise à la fois les axes X et Y :

import { FixedSizeGrid } from 'react-window';

function VirtualMatrix({ rows, cols }) {
  return (
    <FixedSizeGrid
      columnCount={cols}
      columnWidth={150}
      height={500}
      rowCount={rows}
      rowHeight={40}
      width="100%"
    >
      {({ columnIndex, rowIndex, style }) => (
        <div style={style} className="matrix-cell">
          Cellule [{rowIndex}, {columnIndex}]
        </div>
      )}
    </FixedSizeGrid>
  );
}

Optimisations Critiques pour les Performances

Pour garantir une expérience utilisateur irréprochable, même avec des centaines de milliers de lignes, appliquez ces ajustements :

1. Ajustement du Pré-rendu (Overscan)

La propriété overscanCount définit le nombre d'éléments rendus en dehors de la zone visible. Augmenter cette valeur (par exemple à 10 ou 15) réduit les clignotements lors de défilements rapides, au prix d'un léger coût mémoire initial.

<FixedSizeList overscanCount={15} ... />

2. Isolation des Composants Enfants avec React.memo

Si vos lignes contiennent des sous-composants complexes, enveloppez-les dans React.memo pour bloquer les rendus inutiles lorsque leurs props n'ont pas changé :

const ComplexRow = React.memo(({ data, style }) => (
  <div style={style}>
    <HeavyChart data={data.metrics} />
  </div>
));

3. Délégation des Événements

Évitez d'attacher des gestionnaires d'événements (comme onClick) directement sur chaque ligne virtuelle si la liste est immense. Préférez la délégation d'événements sur le conteneur parenet ou utilisez des données data-* pour identifier la cible.

4. Pagination et Défilement Infini

Pour des jeux de données de taille inconnue ou dépassant les limites de la mémoire client, combinez le défilement virtuel avec une logique de chargement progressif (comme react-window-infinite-loader). Cela permet de requêter l'API uniquement lorsque l'utilisateur approche de la fin de la liste actuellement chargée.

Étiquettes: React react-window virtual-scrolling react-hooks performance-optimization

Publié le 21 octobre à 09h28