Virtualizzare elenchi di grandi dimensioni con la finestra di reazione

Tabelle ed elenchi molto grandi possono rallentare in modo significativo le prestazioni del tuo sito. La virtualizzazione può aiutare.

react-window è una libreria che consente il rendering efficiente di elenchi di grandi dimensioni.

Ecco un esempio di elenco contenente 1000 righe visualizzate con react-window. Prova a scorrere il più velocemente possibile.

Perché è utile?

A volte potrebbe essere necessario visualizzare una tabella o un elenco di grandi dimensioni che contengono molte righe. Il caricamento di ogni singolo elemento di questo elenco può influire notevolmente sulle prestazioni.

La virtualizzazione degli elenchi, o "windowing", è il concetto di rendering solo di ciò che è visibile all'utente. Il numero di elementi visualizzati inizialmente è un sottoinsieme molto ridotto dell'intero elenco e la "finestra" dei contenuti visibili quando l'utente continua a scorrere. Questo migliora le prestazioni di rendering e scorrimento dell'elenco.

Finestra di contenuti in un elenco virtualizzato
Spostamento di una "finestra" di contenuti in un elenco virtualizzato

I nodi DOM che escono dalla "finestra" vengono riciclati o sostituiti immediatamente con elementi più recenti man mano che l'utente scorre l'elenco verso il basso. In questo modo il numero di tutti gli elementi sottoposti a rendering rimane specifico per le dimensioni della finestra.

finestra di reazione

react-window è una piccola libreria di terze parti che semplifica la creazione di elenchi virtualizzati nella tua applicazione. Offre una serie di API di base che possono essere utilizzate per diversi tipi di elenchi e tabelle.

Quando utilizzare elenchi con dimensioni fisse

Utilizza il componente FixedSizeList se hai un lungo elenco unidimensionale di elementi di dimensioni uguali.

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;
  • Il componente FixedSizeList accetta le proposte di height, width e itemSize per controllare le dimensioni degli elementi nell'elenco.
  • Una funzione che esegue il rendering delle righe viene passata come elemento figlio a FixedSizeList. È possibile accedere ai dettagli dell'elemento specifico con l'argomento index (items[index]).
  • Un parametro style viene trasmesso anche al metodo di rendering della riga che deve essere associato all'elemento di riga. Gli elementi dell'elenco sono posizionati in modo assoluto con i relativi valori di altezza e larghezza assegnati in linea e il parametro style se ne occupa.

L'esempio di Glitch mostrato in precedenza in questo articolo mostra un esempio di componente FixedSizeList.

Quando utilizzare gli elenchi di dimensioni variabili

Utilizza il componente VariableSizeList per eseguire il rendering di un elenco di elementi di dimensioni diverse. Questo componente funziona come un elenco con dimensioni fisse, ma prevede una funzione per la proposta itemSize anziché un valore specifico.

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'incorporamento seguente mostra un esempio di questo componente.

La funzione per le dimensioni dell'elemento passata al suggerimento itemSize genera in modo casuale l'altezza delle righe in questo esempio. In un'applicazione reale, tuttavia, dovrebbe esistere una logica reale che definisce le dimensioni di ogni elemento. Idealmente, queste dimensioni dovrebbero essere calcolate in base ai dati o ottenuti da un'API.

Griglie

react-window fornisce anche il supporto per la virtualizzazione di elenchi multidimensionali, o griglie. In questo contesto, la "finestra" dei contenuti visibili cambia quando l'utente scorre orizzontalmente e verticalmente.

La finestra mobile dei contenuti in una griglia virtualizzata è bidimensionale
Lo spostamento di una "finestra" di contenuti in una griglia virtualizzata avviene in bidimensionale

Allo stesso modo, è possibile utilizzare entrambi i componenti FixedSizeGrid e VariableSizeGrid a seconda che le dimensioni di elementi specifici dell'elenco possano variare.

  • Per FixedSizeGrid, l'API è praticamente la stessa, ma con il fatto che altezze, larghezze e numero di elementi devono essere rappresentati sia per le colonne che per le righe.
  • Per VariableSizeGrid, sia la larghezza delle colonne che l'altezza delle righe possono essere modificate trastribuendo funzioni anziché valori alle rispettive proposte.

Dai un'occhiata alla documentazione per vedere alcuni esempi di griglie virtualizzate.

Caricamento lento con scorrimento

Molti siti web migliorano le prestazioni aspettando di caricare e visualizzare gli elementi in un lungo elenco fino a quando l'utente non ha fatto scorrere la pagina verso il basso. Questa tecnica, comunemente nota come "caricamento infinito", aggiunge nuovi nodi DOM all'elenco man mano che l'utente scorre oltre una determinata soglia verso la fine. Anche se è meglio che caricare contemporaneamente tutti gli elementi di un elenco, finisce comunque per compilare il DOM con migliaia di voci di riga se l'utente ha superato questo numero. Ciò può portare a un DOM di dimensioni eccessivamente grandi, che inizia a influire sulle prestazioni rallentando i calcoli dello stile e le mutazioni del DOM.

Il seguente diagramma può aiutarti a riassumere questo concetto:

Differenza nello scorrimento tra un elenco normale e un elenco virtualizzato
Differenza nello scorrimento tra un elenco normale e un elenco virtualizzato

L'approccio migliore per risolvere questo problema è continuare a utilizzare una libreria come react-window per mantenere una piccola "finestra" di elementi sulla pagina, ma anche per caricare lentamente le voci più recenti quando l'utente scorre verso il basso. Un pacchetto separato, react-window-infinite-loader, rende possibile tutto ciò con react-window.

Considera la seguente porzione di codice che mostra un esempio di stato gestito in un componente App padre.

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;

Un metodo loadMore viene trasmesso in un ListComponent secondario che contiene l'elenco del caricatore infinito. Questo è importante perché il caricatore infinito deve attivare un callback per caricare più elementi dopo che l'utente ha fatto scorrere oltre un determinato punto.

Ecco il possibile aspetto dell'elemento ListComponent che esegue il rendering dell'elenco:

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;

Qui, il componente FixedSizeList è aggregato all'interno di InfiniteLoader. Gli oggetti di scena assegnati al caricatore sono:

  • isItemLoaded: metodo che verifica se un determinato elemento è stato caricato
  • itemCount: numero di elementi nell'elenco (o previsto)
  • loadMoreItems: callback che restituisce una promessa che si risolve in dati aggiuntivi per l'elenco

Un proposta di rendering viene utilizzata per restituire una funzione utilizzata dal componente elenco per il rendering. Entrambi gli attributi onItemsRendered e ref sono attributi che devono essere trasmessi.

Di seguito è riportato un esempio di come il caricamento infinito può funzionare con un elenco virtualizzato.

Scorrere verso il basso potrebbe sembrare lo stesso, ma ora viene fatta una richiesta di recupero di 10 utenti da un'API utente casuale ogni volta che scorri verso la fine dell'elenco. Tutto questo viene fatto visualizzando solo una singola "finestra" di risultati alla volta.

Se controlli il valore index di un determinato elemento, è possibile che venga visualizzato uno stato di caricamento diverso a seconda che sia stata effettuata una richiesta per voci più recenti e l'elemento sia ancora in fase di caricamento.

Ad esempio:

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

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

Eccesso di scansione

Poiché gli elementi in un elenco virtualizzato cambiano solo quando l'utente scorre, lo spazio vuoto può lampeggiare brevemente mentre stanno per essere visualizzate voci più recenti. Puoi provare a scorrere rapidamente uno degli esempi precedenti in questa guida per notare questo problema.

Per migliorare l'esperienza utente degli elenchi virtualizzati, react-window ti consente di eseguire la scansione eccessiva degli elementi con la proprietà overscanCount. In questo modo puoi definire il numero di elementi al di fuori della "finestra" visibile da visualizzare sempre.

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

overscanCount funziona per entrambi i componenti FixedSizeList e VariableSizeList e ha un valore predefinito di 1. A seconda delle dimensioni di un elenco e di ogni elemento, l'overscaning di più voci può aiutare a evitare un evidente lampo di spazio vuoto quando l'utente scorre. Tuttavia, l'overscansione di troppe voci può influire negativamente sulle prestazioni. L'aspetto fondamentale dell'utilizzo di un elenco virtualizzato è ridurre al minimo il numero di voci in base a ciò che l'utente può vedere in un dato momento, quindi cerca di mantenere il numero di elementi sovradimensionati il più basso possibile.

Per FixedSizeGrid e VariableSizeGrid, utilizza le proprietà overscanColumnsCount e overscanRowsCount per controllare rispettivamente il numero di colonne e di righe di cui eseguire l'overscan.

Conclusione

Se non sai da dove iniziare a virtualizzare elenchi e tabelle nella tua applicazione, segui questi passaggi:

  1. Misura le prestazioni di rendering e scorrimento. Questo articolo mostra come è possibile utilizzare lo strumento di misurazione fPS in Chrome DevTools per esaminare l'efficienza con cui gli elementi vengono visualizzati in un elenco.
  2. Includi react-window per gli elenchi o le griglie lunghi che influiscono sul rendimento.
  3. Se alcune funzionalità non sono supportate in react-window, valuta la possibilità di utilizzare react-virtualized se non puoi aggiungerla autonomamente.
  4. Inserisci react-window-infinite-loader nell'elenco virtualizzato se devi eseguire il caricamento lento degli elementi mentre l'utente scorre.
  5. Utilizza la proprietà overscanCount per i tuoi elenchi e le proprietà overscanColumnsCount e overscanRowsCount per le griglie per evitare la visualizzazione di contenuti vuoti. Non eseguire l'overscansione di troppe voci poiché ciò influirà negativamente sulle prestazioni.