Große Listen mit dem Reaktionsfenster virtualisieren

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

react-window ist eine Bibliothek, mit der große Listen effizient gerendert werden können.

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

Welchen Nutzen bieten sie?

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

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

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

DOM-Knoten, die das „Fenster“ verlassen, werden recycelt oder sofort durch neuere Elemente ersetzt, wenn der Nutzer in der Liste nach unten scrollt. Dadurch wird die Anzahl aller gerenderten Elemente spezifisch für die Größe des Fensters beibehalten.

Reaktionsfenster

react-window ist eine kleine Drittanbieterbibliothek, die das Erstellen virtualisierter Listen in Ihrer Anwendung vereinfacht. Es bietet eine Reihe von Basis-APIs, die für verschiedene Arten von Listen und Tabellen verwendet werden können.

Wann sollte eine Liste mit fester Größe verwendet werden?

Verwenden Sie die Komponente FixedSizeList, wenn Sie eine lange, eindimensionale Liste von Elementen gleicher Größe 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;
  • Für die Komponente FixedSizeList können die Eigenschaften height, width und itemSize verwendet werden, 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.
  • Ein style-Parameter wird außerdem an die Zeilen-Rendering-Methode übergeben, die an das Zeilenelement angehängt werden muss. Listenelemente sind absolut positioniert und ihre Werte für Höhe und Breite werden direkt inline zugewiesen. Dafür ist der Parameter style verantwortlich.

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

Wann werden Listen mit variabler Größe verwendet?

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

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;

Die folgende Einbettung zeigt ein Beispiel für diese Komponente.

Die Funktion für die Elementgröße, die an das Attribut itemSize übergeben wird, ordnet die Zeilenhöhen in diesem Beispiel zufällig an. In einer echten Anwendung sollte es jedoch eine echte Logik geben, die die Größen der einzelnen Elemente definiert. Idealerweise sollten diese Größen anhand von Daten berechnet oder aus einer API bezogen werden.

Raster

react-window unterstützt auch die Virtualisierung mehrdimensionaler Listen oder Raster. In diesem Zusammenhang ändert sich das „Fenster“ sichtbarer Inhalte, wenn der Nutzer horizontal und vertikal scrollt.

Das sich bewegende Inhaltsfenster in einem virtualisierten Raster ist zweidimensional.
Das Verschieben von Inhalten in einem virtualisierten Raster ist zweidimensional.

Ebenso können die Komponenten FixedSizeGrid und VariableSizeGrid verwendet werden, je nachdem, ob die Größe der einzelnen Listenelemente variieren kann.

  • Für FixedSizeGrid ist die API ungefähr die gleiche, allerdings müssen Höhen, Breiten und Anzahlen sowohl für Spalten als auch für Zeilen dargestellt werden.
  • Bei VariableSizeGrid können sowohl die Spaltenbreite als auch die Zeilenhöhe geändert werden, indem Funktionen anstelle von Werten an die entsprechenden Attribute übergeben werden.

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

Lazy Loading beim Scrollen

Viele Websites verbessern die Leistung, indem Elemente in einer langen Liste erst geladen und gerendert werden, bis der Nutzer nach unten gescrollt hat. Bei dieser Technik, die allgemein als "unendliches Laden" bezeichnet wird, werden der Liste neue DOM-Knoten hinzugefügt, wenn der Nutzer bis zum Ende über einen bestimmten Grenzwert scrollt. Obwohl dies besser ist, als alle Elemente einer Liste gleichzeitig zu laden, werden dennoch Tausende von Zeileneinträgen in das DOM gefüllt, wenn der Nutzer an dieser Anzahl vorbeigescrollt hat. Dies kann zu einer übermäßig großen DOM-Größe führen, was wiederum die Leistung beeinträchtigt, da Stilberechnungen und DOM-Mutationen langsamer werden.

Das folgende Diagramm könnte eine Zusammenfassung dazu liefern:

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

Der beste Ansatz zur Lösung dieses Problems besteht darin, weiterhin eine Bibliothek wie react-window zu verwenden, um ein kleines „Fenster“ mit Elementen auf einer Seite beizubehalten, aber auch um neuere Einträge per Lazy Loading zu laden, wenn der Nutzer nach unten scrollt. Das separate Paket react-window-infinite-loader ermöglicht dies mit react-window.

Im folgenden Code wird ein Beispiel für einen Status gezeigt, 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 eine untergeordnete ListComponent übergeben, die die Liste des unendlichen Ladeprogramms 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 hinaus gescrollt hat.

So kann das ListComponent, das die Liste rendert, aussehen:

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 von InfiniteLoader umschlossen. Dem Ladeprogramm sind folgende Requisiten zugewiesen:

  • isItemLoaded: Methode, mit der geprüft wird, ob ein bestimmtes Element geladen wurde
  • itemCount: Anzahl der Elemente auf der Liste (oder erwartete Elemente)
  • loadMoreItems: Callback, der ein Promise zurückgibt, das zu zusätzlichen Daten für die Liste aufgelöst wird

Ein Rendering-Attribut wird verwendet, um eine Funktion zurückzugeben, die die Listenkomponente zum Rendern verwendet. Die Attribute onItemsRendered und ref sind Attribute, die übergeben werden müssen.

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

Das Scrollen in der Liste klingt vielleicht genauso, aber jetzt wird jedes Mal, wenn Sie bis zum Ende der Liste scrollen, eine Anfrage zum Abrufen von 10 Nutzern aus einer zufälligen Nutzer-API gesendet. Dabei wird jeweils nur ein einzelnes "Fenster" mit Ergebnissen gerendert.

Durch Prüfen des index eines bestimmten Elements kann ein anderer Ladestatus für ein Element angezeigt werden, je nachdem, ob eine Anfrage für neuere Einträge gestellt wurde und das Element noch geladen wird.

Beispiel:

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

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

Overscan

Da sich Elemente in einer virtualisierten Liste nur ändern, wenn der Nutzer scrollt, kann der leere Bereich kurz blinken, wenn neuere Einträge angezeigt werden. Sie können versuchen, schnell durch eines der vorherigen Beispiele in diesem Leitfaden zu scrollen, um dies zu bemerken.

Zur Verbesserung der Nutzerfreundlichkeit virtualisierter Listen können Sie react-window Elemente mit der Eigenschaft overscanCount überscannen. So können Sie festlegen, wie viele Elemente außerhalb des sichtbaren „Fensters“ jederzeit gerendert werden sollen.

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

overscanCount funktioniert für die Komponenten FixedSizeList und VariableSizeList und hat den Standardwert 1. Je nach Umfang einer Liste und Größe der einzelnen Elemente kann das Überscannen von mehr als nur einem Eintrag verhindern, dass beim Scrollen des Nutzers ein deutlicher leerer Bereich zu sehen ist. Wenn zu viele Einträge jedoch zu viele Scans ausführen, kann sich dies negativ auf die Leistung auswirken. Der Sinn der Verwendung einer virtualisierten Liste besteht darin, die Anzahl der Einträge auf das zu minimieren, was der Nutzer zu einem bestimmten Zeitpunkt sehen kann. Versuchen Sie daher, die Anzahl der Overscans 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 für den Overscan zu steuern.

Fazit

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

  1. Messen Sie die Rendering- und Scroll-Leistung. In diesem Artikel wird gezeigt, wie Sie mit dem FPS-Messtool in den Chrome-Entwicklertools untersuchen können, wie effizient Elemente in einer Liste gerendert werden.
  2. Fügen Sie react-window für lange Listen oder Raster ein, die die Leistung beeinträchtigen.
  3. Falls bestimmte Features in react-window nicht unterstützt werden, empfiehlt es sich, react-virtualized zu verwenden, falls Sie diese Funktion nicht selbst hinzufügen können.
  4. Umschließen Sie die virtualisierte Liste mit react-window-infinite-loader, wenn Sie Elemente per Lazy Loading beim Scrollen des Nutzers laden müssen.
  5. Verwenden Sie die Eigenschaft overscanCount für Listen und die Eigenschaften overscanColumnsCount und overscanRowsCount für Ihre Raster, um zu verhindern, dass leere Inhalte zu sehen sind. Überscannen Sie nicht zu viele Einträge, da dies die Leistung beeinträchtigt.