Virtualizzare elenchi di grandi dimensioni con la finestra di reazione

Elenchi e tabelle di grandi dimensioni possono rallentare notevolmente il rendimento del tuo sito. La virtualizzazione può aiutarti.

react-window è una biblioteca che consente di visualizzare in modo efficiente elenchi di grandi dimensioni.

Ecco un esempio di elenco che contiene 1000 righe di cui viene eseguito il rendering con react-window. Prova a scorrere il più velocemente possibile.

Perché è utile?

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

La virtualizzazione dell'elenco, o "visualizzazione in finestra", è il concetto di visualizzare solo ciò che è visibile all'utente. Il numero di elementi visualizzati inizialmente è un sottoinsieme molto piccolo dell'intero elenco e la "finestra" dei contenuti visibili si sposta quando l'utente continua a scorrere. In questo modo, vengono migliorate sia le prestazioni di rendering sia quelle di scorrimento dell'elenco.

Finestra di contenuti in un elenco virtualizzato
Spostamento della "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 verso il basso nell'elenco. In questo modo, il numero di tutti gli elementi visualizzati rimane specifico per le dimensioni della finestra.

react-window

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

Quando utilizzare gli elenchi di dimensioni fisse

Utilizza il componente FixedSizeList se hai un lungo elenco monodimensionale 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 proprietà height, width e itemSize per controllare le dimensioni degli elementi all'interno dell'elenco.
  • Una funzione che esegue il rendering delle righe viene passata come elemento secondario a FixedSizeList. I dettagli di un determinato elemento sono accessibili con l'argomento index (items[index]).
  • Al metodo di rendering della riga viene passato anche un parametro style che deve essere associato all'elemento riga. Le voci dell'elenco sono posizionate in modo assoluto con i valori di altezza e larghezza assegnati in linea e il parametro style è responsabile di questo.

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

Quando utilizzare elenchi di dimensioni variabili

Utilizza il componente VariableSizeList per visualizzare un elenco di articoli di dimensioni diverse. Questo componente funziona come un elenco di 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;

Il seguente incorporamento mostra un esempio di questo componente.

La funzione di dimensione dell'elemento passata all'attributo itemSize randomizza le altezze delle righe in questo esempio. In un'applicazione reale, tuttavia, deve esserci una logica reale che definisce le dimensioni di ogni elemento. Idealmente, queste dimensioni dovrebbero essere calcolate in base ai dati o ottenute da un'API.

Griglie

react-window fornisce anche il supporto per la virtualizzazione di elenchi o griglie multi-dimensionali. In questo contesto, la "finestra" dei contenuti visibili cambia man mano che l'utente scorra orizzontalmente e verticalmente.

La finestra mobile dei contenuti in una griglia virtualizzata è bidimensionale
La "finestra" di contenuti in movimento in una griglia virtualizzata è bidimensionale

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

  • Per FixedSizeGrid, l'API è più o meno la stessa, ma le altezze, le larghezze e i conteggi degli elementi devono essere rappresentati sia per le colonne sia per le righe.
  • Per VariableSizeGrid, sia le larghezze delle colonne sia le altezze delle righe possono essere modificate passando funzioni anziché valori ai rispettivi componenti.

Consulta la documentazione per vedere esempi di griglie virtualizzate.

Caricamento lento con scorrimento

Molti siti web migliorano le prestazioni aspettando di caricare e visualizzare gli elementi di un elenco lungo fino a quando l'utente non scorre verso il basso. Questa tecnica, comunemente nota come "caricamento infinito", aggiunge nuovi nodi DOM all'elenco quando l'utente supera una determinata soglia verso la fine. Anche se è meglio caricare tutti gli elementi di un elenco contemporaneamente, alla fine il DOM viene comunque completato con migliaia di voci di riga se l'utente ha superato questo numero di elementi. Ciò può portare a un DOM di dimensioni eccessive, che inizia a influire sulle prestazioni rallentando i calcoli degli stili e le mutazioni del DOM.

Il seguente diagramma potrebbe essere utile per riassumere il tutto:

Differenza nello scorrimento tra un elenco normale e uno 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 su una pagina, ma anche caricare in modo lazy le voci più recenti quando l'utente scorre verso il basso. Un pacchetto separato, react-window-infinite-loader, rende possibile questa operazione con react-window.

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

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 passato a un ListComponent secondario che contiene l'elenco del caricatore infinito. Questo è importante perché il caricamento infinito deve attivare un callback per caricare altri elementi dopo che l'utente ha superato un determinato punto.

Ecco come può essere il 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 è racchiuso all'interno di InfiniteLoader. Gli elementi assegnati al caricatore sono:

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

Un proprietà di rendering viene utilizzata per restituire una funzione utilizzata dal componente dell'elenco per il rendering. Entrambi gli attributi onItemsRendered e ref devono essere passati.

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

Lo scorrimento verso il basso potrebbe sembrare lo stesso, ma ora viene effettuata una richiesta di recupero di 10 utenti da un'API utente casuale ogni volta che scorri verso la fine dell'elenco. Tutto questo viene eseguito mentre viene visualizzata una sola "finestra" di risultati alla volta.

Selezionando index di un determinato elemento, è possibile visualizzare uno stato di caricamento diverso per un elemento, a seconda che sia stata effettuata una richiesta per le voci più recenti e che 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
  }
};

Overscan

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

Per migliorare l'esperienza utente degli elenchi virtualizzati, react-window consente di eseguire la scansione eccessiva degli elementi con la proprietà overscanCount. In questo modo puoi definire quanti elementi al di fuori della "finestra" visibile devono essere visualizzati in ogni momento.

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

overscanCount funziona sia per i componenti FixedSizeList sia per VariableSizeList e ha un valore predefinito di 1. A seconda delle dimensioni di un elenco e delle dimensioni di ogni elemento, l'esecuzione di un'analisi più approfondita di più di una voce può contribuire a evitare un lampo evidente di spazio vuoto quando l'utente scorre. Tuttavia, eseguire la scansione di un numero eccessivo di voci può influire negativamente sulle prestazioni. L'intero scopo dell'utilizzo di un elenco virtualizzato è ridurre al minimo il numero di voci che l'utente può vedere in un determinato momento, quindi cerca di mantenere il numero di elementi sottoposti a scansione eccessiva il più basso possibile.

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

Conclusione

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

  1. Misura le prestazioni di rendering e scorrimento. Questo articolo mostra come utilizzare il metro FPS in Chrome DevTools per esaminare l'efficienza con cui gli elementi vengono visualizzati in un elenco.
  2. Includi react-window per eventuali elenchi o 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 riesci ad aggiungerle autonomamente.
  4. Inserisci un a capo nell'elenco virtualizzato con react-window-infinite-loader se devi caricare gli elementi in modo lazy man mano che l'utente scorre.
  5. Utilizza la proprietà overscanCount per gli elenchi e le proprietà overscanColumnsCount e overscanRowsCount per le griglie per evitare un flash di contenuti vuoti. Non eseguire la scansione eccessiva di troppe voci, poiché ciò influirà negativamente sul rendimento.