Virtualizzare elenchi di grandi dimensioni con la finestra di reazione

Tabelle ed elenchi di dimensioni molto grandi possono rallentare notevolmente le prestazioni del tuo sito. La virtualizzazione può essere d'aiuto.

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

Perché è utile?

A volte potrebbe 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 degli elenchi, o "windowing", è il concetto di eseguire il rendering solo di ciò che è visibile all'utente. Il numero di elementi di cui viene eseguito il rendering all'inizio è un sottoinsieme molto piccolo dell'intero elenco e la "finestra" dei contenuti visibili si sposta quando l'utente continua a scorrere. In questo modo, migliorano sia il rendering sia le prestazioni di scorrimento dell'elenco.

Finestra di contenuti in un elenco virtualizzato
Spostamento della "finestra" dei 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 di cui è stato eseguito il rendering 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 elenchi di dimensioni fisse

Utilizza il componente FixedSizeList se hai un elenco unidimensionale lungo 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 una 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 figlio a FixedSizeList. È possibile accedere ai dettagli del particolare elemento con l'argomento index (items[index]).
  • Viene passato anche un parametro style al metodo di rendering delle righe che deve essere collegato all'elemento della riga. Gli elementi dell'elenco sono posizionati in modo assoluto con i valori di altezza e larghezza assegnati in linea e il parametro style è responsabile di questa operazione.

Quando utilizzare elenchi di dimensioni variabili

Utilizza il componente VariableSizeList per eseguire il rendering di un elenco di elementi di dimensioni diverse. Questo componente funziona allo stesso modo di un elenco di dimensioni fisse, ma si aspetta una funzione per la proprietà 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;

In questo esempio, la funzione delle dimensioni dell'elemento passata alla proprietà itemSize randomizza le altezze delle righe. In un'applicazione reale, tuttavia, dovrebbe essere presente una logica effettiva che definisce le dimensioni di ogni elemento. Idealmente, queste dimensioni devono essere calcolate in base ai dati o ottenute 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 man mano che l'utente scorre orizzontalmente e verticalmente.

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

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

  • Per FixedSizeGrid, l'API è pressoché 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 le funzioni anziché i valori alle rispettive proprietà.

Consulta la documentazione per vedere esempi di griglie virtualizzate.

Caricamento lento durante lo scorrimento

Molti siti web migliorano le prestazioni attendendo il caricamento e il rendering degli elementi in un elenco lungo fino a quando l'utente non ha scorre verso il basso. Questa tecnica, comunemente chiamata "caricamento infinito", aggiunge nuovi nodi DOM all'elenco man mano che l'utente scorre oltre una determinata soglia vicino alla fine. Sebbene sia meglio che caricare tutti gli elementi di un elenco contemporaneamente, il DOM viene comunque popolato con migliaia di voci di riga se l'utente ha scorre oltre quel numero. Ciò può comportare una dimensione del DOM eccessivamente grande, che inizia a influire sulle prestazioni rallentando i calcoli di stile e le mutazioni del DOM.

Il seguente diagramma può aiutarti a riassumere questo concetto:

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

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

Considera il seguente 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 componente figlio ListComponent che contiene l'elenco del caricatore infinito. Questo è importante perché il caricatore infinito deve attivare un callback per caricare altri elementi una volta che l'utente ha scorre oltre un determinato punto.

Ecco come può apparire 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. Le proprietà assegnate 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

Viene utilizzata una proprietà di rendering per restituire una funzione che il componente dell'elenco utilizza per il rendering. Gli attributi onItemsRendered e ref sono attributi che devono essere passati.

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

Controllando l'index di un determinato elemento, è possibile mostrare 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
  }
};

Overscanning

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

Per migliorare l'esperienza utente degli elenchi virtualizzati, react-window consente di eseguire l'overscanning degli elementi con la proprietà overscanCount. In questo modo puoi definire il numero di elementi al di fuori della "finestra" visibile di cui eseguire il rendering in qualsiasi 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'overscanning di più di una voce può contribuire a evitare un lampeggiamento evidente di spazio vuoto quando l'utente scorre. Tuttavia, l'overscanning di troppe voci può influire negativamente sulle prestazioni. Lo scopo principale dell'utilizzo di un elenco virtualizzato è ridurre al minimo il numero di voci a ciò che l'utente può vedere in un determinato momento, quindi cerca di mantenere il numero di elementi sottoposti a overscanning il più basso possibile.

Per FixedSizeGrid e VariableSizeGrid, utilizza rispettivamente le proprietà overscanColumnsCount e overscanRowsCount per controllare il numero di colonne e righe da sottoporre a overscanning.

Conclusione

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

  1. Misura il rendering e le prestazioni di scorrimento. Questo articolo mostra come utilizzare il misuratore FPS in Chrome DevTools per esplorare l'efficienza con cui viene eseguito il rendering degli elementi in un elenco.
  2. Includi react-window per tutti gli elenchi o le griglie lunghi che influiscono sulle prestazioni.
  3. Se alcune funzionalità non sono supportate in react-window, valuta la possibilità di utilizzare react-virtualized se non puoi aggiungere questa funzionalità autonomamente.
  4. Racchiudi l'elenco virtualizzato con react-window-infinite-loader se devi caricare lentamente gli elementi 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 lampeggiamento di contenuti vuoti. Non eseguire l'overscanning di troppe voci, in quanto ciò influirà negativamente sulle prestazioni.