Virtualiser de longues listes avec une fenêtre de réaction

Les tables et listes très volumineuses peuvent ralentir considérablement les performances de votre site. La virtualisation peut vous aider.

react-window est une bibliothèque qui permet d'afficher efficacement de longues listes.

Voici un exemple de liste contenant 1 000 lignes affichées avec react-window. Essayez de faire défiler la page aussi vite que possible.

En quoi est-ce utile ?

Il peut arriver que vous deviez afficher une grande table ou une liste contenant de nombreuses lignes. Le chargement de chaque élément d'une telle liste peut avoir un impact significatif sur les performances.

La virtualisation de listes, ou "fenêtrage", est le concept qui consiste à n'afficher que ce qui est visible par l'utilisateur. Le nombre d'éléments affichés au début est un très petit sous-ensemble de la liste entière, et la "fenêtre" du contenu visible déplace lorsque l'utilisateur continue de faire défiler la page. Cela améliore les performances de rendu et de défilement de la liste.

Fenêtre de contenu dans une liste virtualisée
Déplacement de la "fenêtre" de contenu dans une liste virtualisée

Les nœuds DOM qui quittent la "fenêtre" sont recyclés ou immédiatement remplacés par des éléments plus récents lorsque l'utilisateur fait défiler la liste. Ainsi, le nombre de tous les éléments affichés est spécifique à la taille de la fenêtre.

react-window

react-window est une petite bibliothèque tierce qui facilite la création de listes virtualisées dans votre application. Elle fournit un certain nombre d'API de base pouvant être utilisées pour différents types de listes et de tableaux.

Quand utiliser des listes de taille fixe ?

Utilisez le composant FixedSizeList si vous disposez d'une longue liste unidimensionnelle d'éléments de taille égale.

import React from 'react';
import { FixedSizeList } from 'react-window';

const items = [...] // some list of items

const Row = ({ index, style }) => (
  <div style={style}>
     {/* define the row component using items[index] */}
  </div>
);

const ListComponent = () => (
  <FixedSizeList
    height={500}
    width={500}
    itemSize={120}
    itemCount={items.length}
  >
    {Row}
  </FixedSizeList>
);

export default ListComponent;
  • Le composant FixedSizeList accepte des propriétés height, width et itemSize pour contrôler la taille des éléments de la liste.
  • Une fonction qui affiche les lignes est transmise en tant qu'enfant à FixedSizeList. Vous pouvez accéder aux détails de l'élément concerné à l'aide de l'argument index (items[index]).
  • Un paramètre style est également transmis à la méthode de rendu de la ligne qui doit être associée à l'élément de ligne. Les éléments de liste sont positionnés de manière absolue, et leurs valeurs de hauteur et de largeur sont attribuées en ligne. C'est le paramètre style qui s'en charge.

L'exemple Glitch présenté plus tôt dans cet article illustre un composant FixedSizeList.

Quand utiliser des listes de taille variable ?

Utilisez le composant VariableSizeList pour afficher une liste d'éléments de différentes tailles. Ce composant fonctionne de la même manière qu'une liste de taille fixe, mais attend plutôt une fonction pour l'attribut itemSize au lieu d'une valeur spécifique.

import React from 'react';
import { VariableSizeList } from 'react-window';

const items = [...] // some list of items

const Row = ({ index, style }) => (
  <div style={style}>
     {/* define the row component using items[index] */}
  </div>
);

const getItemSize = index => {
  // return a size for items[index]
}

const ListComponent = () => (
  <VariableSizeList
    height={500}
    width={500}
    itemCount={items.length}
    itemSize={getItemSize}
  >
    {Row}
  </VariableSizeList>
);

export default ListComponent;

L'intégration suivante montre un exemple de ce composant.

Dans cet exemple, la fonction de taille d'élément transmise à la propriété itemSize répartit de manière aléatoire la hauteur des lignes. Toutefois, dans une application réelle, une logique réelle doit définir les tailles de chaque élément. Idéalement, ces tailles doivent être calculées à partir de données ou obtenues à partir d'une API.

Grilles

react-window permet également de virtualiser des listes ou des grilles multidimensionnelles. Dans ce contexte, la "fenêtre" du contenu visible change à mesure que l'utilisateur fait défiler l'écran horizontalement et verticalement.

La fenêtre de contenu mobile dans une grille virtualisée est bidimensionnelle
Le déplacement de la "fenêtre" de contenu dans une grille virtualisée est bidimensionnel.

De même, vous pouvez utiliser les deux composants FixedSizeGrid et VariableSizeGrid selon que la taille d'éléments de liste spécifiques peut varier ou non.

  • Pour FixedSizeGrid, l'API est à peu près identique, mais avec le fait que les hauteurs, les largeurs et le nombre d'éléments doivent être représentés à la fois pour les colonnes et les lignes.
  • Pour VariableSizeGrid, vous pouvez modifier à la fois la largeur des colonnes et la hauteur des lignes en transmettant des fonctions au lieu de valeurs à leurs props respectifs.

Consultez la documentation pour voir des exemples de grilles virtualisées.

Chargement différé lors du défilement

De nombreux sites Web améliorent leurs performances en attendant de charger et d'afficher les éléments d'une longue liste jusqu'à ce que l'utilisateur ait fait défiler la page vers le bas. Cette technique, communément appelée "chargement infini", ajoute de nouveaux nœuds DOM à la liste lorsque l'utilisateur dépasse un certain seuil proche de la fin. Bien que cela soit préférable à charger tous les éléments d'une liste en même temps, le DOM se remplit toujours de milliers d'entrées de ligne si l'utilisateur a fait défiler la page au-delà de ce nombre. Cela peut entraîner une taille de DOM trop grande, ce qui commence à affecter les performances en ralentissant les calculs de style et les mutations DOM.

Le schéma suivant peut vous aider à résumer:

Différence de défilement entre une liste standard et une liste virtualisée
Différence de défilement entre une liste standard et une liste virtualisée

La meilleure approche pour résoudre ce problème consiste à continuer à utiliser une bibliothèque telle que react-window pour maintenir une petite "fenêtre" d'éléments sur une page, mais aussi pour charger les entrées plus récentes lorsque l'utilisateur fait défiler la page vers le bas. Un package distinct, react-window-infinite-loader, permet cela avec react-window.

Prenons l'extrait de code suivant, qui présente un exemple d'état géré dans un composant App parent.

import React, { Component } from 'react';

import ListComponent from './ListComponent';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      items: [], // instantiate initial list here
      moreItemsLoading: false,
      hasNextPage: true
    };

    this.loadMore = this.loadMore.bind(this);
  }

  loadMore() {
   // method to fetch newer entries for the list
  }

  render() {
    const { items, moreItemsLoading, hasNextPage } = this.state;

    return (
      <ListComponent
        items={items}
        moreItemsLoading={moreItemsLoading}
        loadMore={this.loadMore}
        hasNextPage={hasNextPage}
      />
    );
  }
}

export default App;

Une méthode loadMore est transmise à un ListComponent enfant qui contient la liste de chargeurs infinis. Cela est important, car le chargeur infini doit déclencher un rappel pour charger d'autres éléments une fois que l'utilisateur a fait défiler la page au-delà d'un certain point.

Voici à quoi peut ressembler le ListComponent qui affiche la liste :

import React from 'react';
import { FixedSizeList } from 'react-window';
import InfiniteLoader from "react-window-infinite-loader";

const ListComponent = ({ items, moreItemsLoading, loadMore, hasNextPage }) => {
  const Row = ({ index, style }) => (
     {/* define the row component using items[index] */}
  );

  const itemCount = hasNextPage ? items.length + 1 : items.length;

  return (
    <InfiniteLoader
      isItemLoaded={index => index < items.length}
      itemCount={itemCount}
      loadMoreItems={loadMore}
    >
      {({ onItemsRendered, ref }) => (
        <FixedSizeList
          height={500}
          width={500}
          itemCount={itemCount}
          itemSize={120}
          onItemsRendered={onItemsRendered}
          ref={ref}
        >
          {Row}
        </FixedSizeList>
      )}
  </InfiniteLoader>
  )
};

export default ListComponent;

Ici, le composant FixedSizeList est encapsulé dans InfiniteLoader. Les accessoires attribués au chargeur sont les suivants:

  • isItemLoaded : méthode qui vérifie si un élément donné a été chargé
  • itemCount : nombre d'éléments de la liste (ou attendus)
  • loadMoreItems : rappel qui renvoie une promesse qui se résout en données supplémentaires pour la liste

Un propriété de rendu permet de renvoyer une fonction utilisée par le composant de liste pour l'affichage. Les attributs onItemsRendered et ref doivent être transmis.

Voici un exemple de fonctionnement de la charge infinie avec une liste virtualisée.

Faire défiler la liste peut sembler identique, mais une requête est désormais envoyée pour récupérer 10 utilisateurs d'une API utilisateur aléatoire chaque fois que vous approchez de la fin de la liste. Tout cela se fait en affichant une seule "fenêtre" de résultats à la fois.

En vérifiant le index d'un élément donné, un état de chargement différent peut être affiché pour cet élément, selon qu'une requête a été envoyée pour des entrées plus récentes et que l'élément est toujours en cours de chargement.

Exemple :

const Row = ({ index, style }) => {
  const itemLoading = index === items.length;

  if (itemLoading) {
      // return loading state
  } else {
      // return item
  }
};

Surbalayage

Étant donné que les éléments d'une liste virtualisée ne changent que lorsque l'utilisateur fait défiler la page, l'espace vide peut clignoter brièvement lorsque de nouvelles entrées sont sur le point d'être affichées. Vous pouvez essayer de faire défiler rapidement l'un des exemples précédents de ce guide pour le constater.

Pour améliorer l'expérience utilisateur des listes virtualisées, react-window vous permet de suranalyser les éléments avec la propriété overscanCount. Cela vous permet de définir le nombre d'éléments en dehors de la "fenêtre" visible à afficher en permanence.

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

overscanCount fonctionne à la fois pour les composants FixedSizeList et VariableSizeList, et sa valeur par défaut est 1. En fonction de la taille d'une liste et de la taille de chaque élément, le surbalayage de plusieurs entrées peut aider à éviter un flash d'espace vide visible lorsque l'utilisateur fait défiler la page. Toutefois, le surbalayage d'un trop grand nombre d'entrées peut nuire aux performances. L'objectif d'une liste virtualisée est de réduire le nombre d'entrées à ce que l'utilisateur peut voir à un moment donné. Essayez donc de limiter le nombre d'éléments surnumérisés au minimum.

Pour FixedSizeGrid et VariableSizeGrid, utilisez les propriétés overscanColumnsCount et overscanRowsCount pour contrôler respectivement le nombre de colonnes et de lignes à suranalyser.

Conclusion

Si vous ne savez pas par où commencer à virtualiser les listes et les tableaux de votre application, procédez comme suit :

  1. Mesurez les performances de rendu et de défilement. Cet article explique comment utiliser le mètre FPS des outils pour les développeurs Chrome pour évaluer l'efficacité d'affichage des éléments d'une liste.
  2. Incluez react-window pour toutes les longues listes ou grilles qui affectent les performances.
  3. Si certaines fonctionnalités ne sont pas compatibles avec react-window, envisagez d'utiliser react-virtualized si vous ne pouvez pas ajouter cette fonctionnalité vous-même.
  4. Encapsulez votre liste virtualisée avec react-window-infinite-loader si vous devez charger des éléments de manière différée lorsque l'utilisateur fait défiler la page.
  5. Utilisez la propriété overscanCount pour vos listes, et les propriétés overscanColumnsCount et overscanRowsCount pour vos grilles afin d'éviter une apparition de contenu vide. N'effectuez pas de balayage excessif sur un trop grand nombre d'entrées, car cela affectera négativement les performances.