Wirtualizacja dużych list za pomocą okna reakcji

Bardzo duże tabele i listy mogą znacznie spowolnić działanie witryny. Wirtualizacja może pomóc.

react-window to biblioteka, która umożliwia wydajne renderowanie dużych list.

Dlaczego jest to przydatne?

Czasami trzeba wyświetlić dużą tabelę lub listę zawierającą wiele wierszy. Wczytywanie każdego elementu na takiej liście może znacząco wpłynąć na wydajność.

Wirtualizacja listy, czyli "okienkowanie", polega na renderowaniu tylko tego, co jest widoczne dla użytkownika. Liczba elementów renderowanych na początku to bardzo mały podzbiór całej listy, a „okno” widocznej treści przesuwa się , gdy użytkownik przewija listę. Poprawia to zarówno renderowanie, jak i przewijanie listy.

Okno treści na liście wirtualnej
Przesuwanie "okna" treści na zwirtualizowanej liście

Węzły DOM, które wychodzą poza „okno”, są ponownie wykorzystywane lub natychmiast zastępowane nowszymi elementami, gdy użytkownik przewija listę w dół. Dzięki temu liczba wszystkich renderowanych elementów jest dostosowana do rozmiaru okna.

react-window

react-window to niewielka biblioteka innej firmy, która ułatwia tworzenie zwirtualizowanych list w aplikacji. Zawiera ona kilka podstawowych interfejsów API, których można używać w przypadku różnych typów list i tabel.

Kiedy używać list o stałym rozmiarze

Jeśli masz długą, jednowymiarową listę elementów o jednakowym rozmiarze, użyj komponentu FixedSizeList.

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;
  • Komponent FixedSizeList akceptuje właściwości height, width i itemSize, które pozwalają kontrolować rozmiar elementów na liście.
  • Funkcja renderująca wiersze jest przekazywana jako element podrzędny do komponentu FixedSizeList. Dostęp do szczegółów konkretnego elementu można uzyskać za pomocą argumentu index (items[index]).
  • Do metody renderowania wiersza przekazywany jest też parametr style, który musi być dołączony do elementu wiersza. Elementy listy są pozycjonowane bezwzględnie, a ich wysokość i szerokość są przypisywane w tekście. Odpowiada za to parametr style.

Kiedy używać list o zmiennym rozmiarze

Aby renderować listę elementów o różnych rozmiarach, użyj komponentu VariableSizeList. Ten komponent działa tak samo jak lista o stałym rozmiarze, ale zamiast konkretnej wartości oczekuje funkcji dla właściwości 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;

Funkcja rozmiaru elementu przekazana do właściwości itemSize losowo określa wysokość wierszy w tym przykładzie. W prawdziwej aplikacji powinna jednak istnieć rzeczywista logika określająca rozmiary poszczególnych elementów. Najlepiej, aby te rozmiary były obliczane na podstawie danych lub pobierane z interfejsu API.

Siatki

react-window obsługuje też wirtualizację list wielowymiarowych, czyli siatek. W tym kontekście „okno” widocznej treści zmienia się, gdy użytkownik przewija w poziomie i w pionie.

Przesuwne okno treści w zwirtualizowanej siatce jest dwuwymiarowe
Przesuwanie "okna" treści w zwirtualizowanej siatce jest dwuwymiarowe

Podobnie, w zależności od tego, czy rozmiar poszczególnych elementów listy może się różnić, można używać komponentów FixedSizeGrid i VariableSizeGrid.

  • W przypadku komponentu FixedSizeGrid interfejs API jest prawie taki sam, ale trzeba pamiętać, że wysokość, szerokość i liczba elementów muszą być reprezentowane zarówno w przypadku kolumn, jak i wierszy.
  • W przypadku komponentu VariableSizeGrid szerokość kolumn i wysokość wierszy można zmieniać, przekazując funkcje zamiast wartości do odpowiednich właściwości.

Przykłady zwirtualizowanych siatek znajdziesz w dokumentacji aby zobaczyć.

Leniwe ładowanie podczas przewijania

Wiele witryn zwiększa wydajność, czekając z wczytaniem i renderowaniem elementów na długiej liście, aż użytkownik przewinie ją w dół. Ta technika, powszechnie nazywana „nieskończonym ładowaniem”, dodaje nowe węzły DOM do listy, gdy użytkownik przewinie ją do określonego progu blisko końca. Chociaż jest to lepsze rozwiązanie niż wczytywanie wszystkich elementów na liście naraz, nadal powoduje zapełnianie DOM tysiącami wierszy, jeśli użytkownik przewinie listę do tego miejsca. Może to prowadzić do nadmiernego powiększenia DOM, co zaczyna wpływać na wydajność, spowalniając obliczenia stylów i mutacje DOM.

Poniższy diagram może pomóc w podsumowaniu tego:

Różnica w przewijaniu między zwykłą a zwirtualizowaną listą
Różnica w przewijaniu między zwykłą a zwirtualizowaną listą

Najlepszym rozwiązaniem tego problemu jest dalsze używanie biblioteki takiej jak react-window, aby utrzymywać małe „okno” elementów na stronie, ale też leniwe ładowanie nowszych wpisów, gdy użytkownik przewija listę w dół. Umożliwia to oddzielny pakiet react-window-infinite-loader.react-window

Rozważmy ten fragment kodu, który pokazuje przykład stanu zarządzanego w komponencie nadrzędnym App.

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;

Metoda loadMore jest przekazywana do komponentu podrzędnego ListComponent, który zawiera listę z nieskończonym ładowaniem. Jest to ważne, ponieważ nieskończony moduł wczytujący musi uruchomić wywołanie zwrotne, aby wczytać więcej elementów, gdy użytkownik przewinie listę do określonego miejsca.

Oto jak może wyglądać komponent ListComponent, który renderuje listę:

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;

W tym przypadku komponent FixedSizeList jest opakowany w komponencie InfiniteLoader. Właściwości przypisane do modułu wczytującego to:

  • isItemLoaded: metoda, która sprawdza, czy dany element został wczytany.
  • itemCount: liczba elementów na liście (lub oczekiwana).
  • loadMoreItems: funkcja zwrotna, która zwraca obietnicę, że zostanie rozwiązana w przypadku dodatkowych danych na liście.

Właściwość renderowania służy do zwracania funkcji, której komponent listy używa do renderowania. Trzeba przekazać atrybuty onItemsRendered i ref.

Przewijanie listy w dół może wyglądać tak samo, ale za każdym razem, gdy przewiniesz listę do końca, wysyłane jest żądanie pobrania 10 użytkowników z losowego interfejsu API użytkownika. Wszystko to odbywa się przy renderowaniu tylko jednego „okna” wyników naraz.

Sprawdzając index danego elementu, można wyświetlić inny stan wczytywania w zależności od tego, czy wysłano żądanie nowszych wpisów i czy element jest nadal wczytywany.

Na przykład:

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

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

Nadmierne skanowanie

Ponieważ elementy na zwirtualizowanej liście zmieniają się tylko wtedy, gdy użytkownik przewija, gdy mają się wyświetlić nowsze wpisy, może na chwilę pojawić się puste miejsce. Aby to zauważyć, możesz szybko przewinąć dowolny z poprzednich przykładów w tym przewodniku.

Aby poprawić komfort użytkowników korzystających ze zwirtualizowanych list, biblioteka react-window umożliwia nadmierne skanowanie elementów za pomocą właściwości overscanCount. Pozwala to określić, ile elementów poza widocznym „oknem” ma być renderowanych przez cały czas.

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

Właściwość overscanCount działa zarówno w przypadku komponentów FixedSizeList, jak i VariableSizeList i ma wartość domyślną 1. W zależności od tego, jak duża jest lista i jak duży jest każdy element, nadmierne skanowanie więcej niż 1 wpisu może pomóc zapobiec zauważalnemu miganiu pustego miejsca, gdy użytkownik przewija listę. Nadmierne skanowanie zbyt wielu wpisów może jednak negatywnie wpłynąć na wydajność. Celem używania zwirtualizowanej listy jest zminimalizowanie liczby wpisów do tego, co użytkownik może zobaczyć w danym momencie, dlatego staraj się, aby liczba nadmiernie skanowanych elementów była jak najmniejsza.

W przypadku komponentów FixedSizeGrid i VariableSizeGrid użyj właściwości overscanColumnsCount i overscanRowsCount, aby kontrolować odpowiednio liczbę kolumn i wierszy do nadmiernego skanowania.

Podsumowanie

Jeśli nie wiesz, od czego zacząć wirtualizację list i tabel w aplikacji, wykonaj te czynności:

  1. Zmierz wydajność renderowania i przewijania. W tym artykule pokazujemy, jak używać miernika FPS w Narzędziach deweloperskich w Chrome, aby sprawdzić, jak wydajnie renderowane są elementy na liście.
  2. Dodaj bibliotekę react-window do wszystkich długich list lub siatek, które wpływają na wydajność.
  3. Jeśli niektóre funkcje nie są obsługiwane w react-window, rozważ użycie react-virtualized jeśli nie możesz samodzielnie dodać tej funkcji.
  4. Jeśli musisz leniwie wczytywać elementy, gdy użytkownik przewija listę, opakuj zwirtualizowaną listę za pomocą biblioteki react-window-infinite-loader.
  5. Użyj właściwości overscanCount w przypadku list oraz właściwości overscanColumnsCount i overscanRowsCount w przypadku siatek, aby zapobiec miganiu pustej treści. Nie skanuj nadmiernie zbyt wielu wpisów, ponieważ wpłynie to negatywnie na wydajność.