Große Listen mit dem Reaktionsfenster virtualisieren

Sehr große Tabellen und Listen können die Leistung Ihrer Website erheblich verlangsamen. Virtualisierung kann helfen.

react-window ist eine Bibliothek, mit der sich große Listen effizient rendern lassen.

Hier sehen Sie ein Beispiel für eine Liste mit 1.000 Zeilen, die mit react-window gerendert wird. Scrollen Sie so schnell wie möglich.

Welchen Nutzen bieten sie?

Manchmal müssen Sie möglicherweise eine große Tabelle oder Liste mit vielen Zeilen anzeigen. Das Laden jedes einzelnen Elements in einer solchen Liste kann die Leistung erheblich beeinträchtigen.

Bei der Listenvirtualisierung oder dem „Windowing“ wird nur das gerendert, was für den Nutzer sichtbar ist. Die Anzahl der Elemente, die zuerst gerendert werden, ist eine sehr kleine Teilmenge der gesamten Liste. Das „Fenster“ mit sichtbaren Inhalten bewegt sich, wenn der Nutzer weiter scrollt. Dadurch wird sowohl die Rendering- als auch die Scrollleistung der Liste verbessert.

Fenster mit Inhalten in einer virtualisierten Liste
„Fenster“ mit Inhalten in einer virtualisierten Liste verschieben

DOM-Knoten, die das „Fenster“ verlassen, werden wiederverwendet oder sofort durch neuere Elemente ersetzt, wenn der Nutzer in der Liste nach unten scrollt. So bleibt die Anzahl aller gerenderten Elemente an die Größe des Fensters gebunden.

react-window

react-window ist eine kleine Drittanbieterbibliothek, mit der sich virtualisierte Listen in Ihrer Anwendung einfacher erstellen lassen. Sie bietet eine Reihe von Basis-APIs, die für verschiedene Arten von Listen und Tabellen verwendet werden können.

Wann sollten Listen mit fester Größe verwendet werden?

Verwenden Sie die Komponente FixedSizeList, wenn Sie eine lange, eindimensionale Liste mit gleich großen Elementen haben.

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;
  • Die Komponente FixedSizeList akzeptiert die Attribute height, width und itemSize, um die Größe der Elemente in der Liste zu steuern.
  • Eine Funktion, die die Zeilen rendert, wird als untergeordnetes Element an FixedSizeList übergeben. Details zum jeweiligen Element können mit dem Argument index (items[index]) abgerufen werden.
  • Außerdem wird ein style-Parameter an die Methode zum Rendern der Zeile übergeben, der unbedingt an das Zeilenelement angehängt werden muss. Listenelemente werden absolut positioniert, wobei ihre Höhe und Breite inline zugewiesen werden. Der Parameter style ist dafür verantwortlich.

Das Glitch-Beispiel, das weiter oben in diesem Artikel gezeigt wird, enthält ein Beispiel für eine FixedSizeList-Komponente.

Wann sollten Listen mit variabler Größe verwendet werden?

Mit der Komponente VariableSizeList können Sie eine Liste von Elementen mit unterschiedlichen Größen rendern. Diese Komponente funktioniert genauso wie eine Liste mit fester Größe, erwartet aber anstelle eines bestimmten Werts eine Funktion für die itemSize-Eigenschaft.

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;

Das folgende eingebettete Element zeigt ein Beispiel für diese Komponente.

Die Funktion für die Artikelgröße, die an die itemSize-Eigenschaft übergeben wird, randomisiert in diesem Beispiel die Zeilenhöhen. In einer echten Anwendung sollte jedoch eine tatsächliche Logik vorhanden sein, die die Größe der einzelnen Elemente definiert. Idealerweise sollten diese Größen auf Grundlage von Daten berechnet oder über eine API abgerufen werden.

Grids

react-window unterstützt auch die Virtualisierung mehrdimensionaler Listen oder Tabellen. In diesem Kontext ändert sich das „Fenster“ mit sichtbaren Inhalten, wenn der Nutzer horizontal und vertikal scrollt.

Das bewegliche Fenster mit Inhalten in einem virtualisierten Raster ist zweidimensional.
Das Verschieben des „Fensters“ mit Inhalten in einem virtualisierten Raster ist zweidimensional.

Je nachdem, ob die Größe bestimmter Listenelemente variieren kann, können auch die Komponenten FixedSizeGrid und VariableSizeGrid verwendet werden.

  • Für FixedSizeGrid ist die API in etwa gleich, nur dass Höhen, Breiten und Anzahl der Elemente sowohl für Spalten als auch für Zeilen angegeben werden müssen.
  • Bei VariableSizeGrid können sowohl die Spaltenbreiten als auch die Zeilenhöhen geändert werden, indem Funktionen anstelle von Werten an die entsprechenden Attribute übergeben werden.

In der Dokumentation finden Sie Beispiele für virtualisierte Grids.

Lazy Loading beim Scrollen

Viele Websites verbessern die Leistung, indem sie das Laden und Rendern von Elementen in einer langen Liste verzögern, bis der Nutzer nach unten gescrollt hat. Bei dieser Technik, die häufig als „unendliches Laden“ bezeichnet wird, werden der Liste neue DOM-Knoten hinzugefügt, wenn der Nutzer an einem bestimmten Schwellenwert in der Nähe des Endes vorbeiscrollt. Das ist zwar besser, als alle Elemente einer Liste auf einmal zu laden, aber wenn der Nutzer so weit scrollt, dass Tausende von Zeileneinträgen angezeigt werden, wird das DOM trotzdem mit Tausenden von Einträgen gefüllt. Dies kann zu einer übermäßig großen DOM-Größe führen, was sich auf die Leistung auswirkt, da Stilberechnungen und DOM-Mutationen langsamer werden.

Das folgende Diagramm fasst dies zusammen:

Unterschied beim Scrollen zwischen einer regulären und einer virtualisierten Liste
Unterschied beim Scrollen zwischen einer regulären und einer virtualisierten Liste

Die beste Lösung für dieses Problem ist, weiterhin eine Bibliothek wie react-window zu verwenden, um ein kleines „Fenster“ von Elementen auf einer Seite beizubehalten, aber auch neuere Einträge nachzuladen, wenn der Nutzer nach unten scrollt. Ein separates Paket, react-window-infinite-loader, macht dies mit react-window möglich.

Sehen Sie sich den folgenden Code an, der ein Beispiel für den Status zeigt, der in einer übergeordneten App-Komponente verwaltet wird.

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;

Eine loadMore-Methode wird an ein untergeordnetes ListComponent übergeben, das die unendliche Ladeliste enthält. Das ist wichtig, weil der unendliche Loader einen Callback auslösen muss, um weitere Elemente zu laden, sobald der Nutzer über einen bestimmten Punkt hinausgescrollt ist.

So kann der ListComponent aussehen, mit dem die Liste gerendert wird:

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;

Hier ist die Komponente FixedSizeList in InfiniteLoader eingeschlossen. Dem Loader zugewiesene Attribute:

  • isItemLoaded: Methode, mit der geprüft wird, ob ein bestimmter Artikel geladen wurde
  • itemCount: Anzahl der Elemente in der Liste (oder erwartet)
  • loadMoreItems: Callback, der ein Promise zurückgibt, das in zusätzliche Daten für die Liste aufgelöst wird.

Eine Render-Eigenschaft wird verwendet, um eine Funktion zurückzugeben, die von der Listenkomponente zum Rendern verwendet wird. Die Attribute onItemsRendered und ref müssen übergeben werden.

Das folgende Beispiel zeigt, wie unendliches Laden mit einer virtualisierten Liste funktionieren kann.

Das Scrollen in der Liste fühlt sich möglicherweise gleich an, aber jedes Mal, wenn Sie zum Ende der Liste scrollen, wird eine Anfrage gesendet, um 10 Nutzer aus einer Random User API abzurufen. Dabei wird immer nur ein „Fenster“ mit Ergebnissen gerendert.

Wenn Sie die index eines bestimmten Elements prüfen, kann je nach dem, ob eine Anfrage für neuere Einträge gestellt wurde und das Element noch geladen wird, ein anderer Ladestatus für ein Element angezeigt werden.

Beispiel:

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

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

Overscanning

Da sich Elemente in einer virtualisierten Liste nur ändern, wenn der Nutzer scrollt, kann es kurz zu einem leeren Bereich kommen, wenn neue Einträge angezeigt werden. Wenn Sie in diesem Leitfaden schnell durch eines der vorherigen Beispiele scrollen, können Sie das sehen.

Zur Verbesserung der Nutzerfreundlichkeit von virtualisierten Listen können Sie mit react-window Elemente mit der Eigenschaft overscanCount über den sichtbaren Bereich hinaus rendern. So können Sie festlegen, wie viele Elemente außerhalb des sichtbaren „Fensters“ gerendert werden sollen.

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

overscanCount funktioniert sowohl für die Komponenten FixedSizeList als auch VariableSizeList und hat einen Standardwert von 1. Je nach Größe einer Liste und der Größe der einzelnen Elemente kann es sinnvoll sein, mehr als nur einen Eintrag zu überscannen, um ein sichtbares Aufblitzen von leerem Raum beim Scrollen zu verhindern. Wenn Sie jedoch zu viele Einträge durchsuchen, kann sich das negativ auf die Leistung auswirken. Der Sinn einer virtualisierten Liste besteht darin, die Anzahl der Einträge auf das zu beschränken, was der Nutzer zu einem bestimmten Zeitpunkt sehen kann. Versuchen Sie also, die Anzahl der überscannten Elemente so gering wie möglich zu halten.

Verwenden Sie für FixedSizeGrid und VariableSizeGrid die Attribute overscanColumnsCount und overscanRowsCount, um die Anzahl der Spalten bzw. Zeilen zu steuern, die über den Bildschirmrand hinaus angezeigt werden sollen.

Fazit

Wenn Sie sich nicht sicher sind, wo Sie mit der Virtualisierung von Listen und Tabellen in Ihrer Anwendung beginnen sollen, gehen Sie so vor:

  1. Leistung beim Rendern und Scrollen messen In diesem Artikel wird beschrieben, wie Sie mit dem FPS-Zähler in den Chrome-Entwicklertools untersuchen können, wie effizient Elemente in einer Liste gerendert werden.
  2. Fügen Sie react-window für alle langen Listen oder Tabellen ein, die sich auf die Leistung auswirken.
  3. Wenn bestimmte Funktionen in react-window nicht unterstützt werden, sollten Sie react-virtualized verwenden, falls Sie diese Funktionen nicht selbst hinzufügen können.
  4. Schließen Sie die virtualisierte Liste in react-window-infinite-loader ein, wenn Sie Elemente beim Scrollen des Nutzers verzögert laden müssen.
  5. Verwenden Sie die overscanCount-Property für Ihre Listen und die overscanColumnsCount- und overscanRowsCount-Properties für Ihre Grids, um zu verhindern, dass kurzzeitig leerer Inhalt angezeigt wird. Scannen Sie nicht zu viele Einträge, da dies die Leistung beeinträchtigt.