Wirtualizacja dużych list za pomocą okna reakcji

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

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

Oto przykład listy zawierającej 1000 wierszy renderowanych za pomocą funkcji react-window. Spróbuj przewinąć stronę jak najszybciej.

Dlaczego to jest przydatne?

Czasami może być konieczne wyświetlenie dużej tabeli lub listy zawierającej wiele wierszy. Wczytanie każdego elementu na takiej liście może znacząco wpłynąć na skuteczność.

Wirtualizacja list, czyli „okno”, to koncepcja renderowania 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” widocznych treści przesuwa się, gdy użytkownik kontynuuje przewijanie. Dzięki temu poprawisz wydajność listy zarówno pod względem renderowania, jak i przewijania.

Okno treści na liście wirtualnej
Przenoszenie „okna” treści na zwirtualizowanej liście

Węzły DOM, z których wychodzi „okno”, są poddawane recyklingowi lub natychmiast są zastępowane nowymi elementami, gdy użytkownik przewinie stronę w dół. Dzięki temu liczba wszystkich wyrenderowanych elementów jest dostosowana do rozmiaru okna.

react-window

react-window to niewielka biblioteka innej firmy, która ułatwia tworzenie w aplikacji wirtualnych list. Udostępnia ona kilka podstawowych interfejsów API, które można stosować 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 takim samym 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 przyjmuje właściwości height, widthitemSize, aby kontrolować rozmiar elementów na liście.
  • Funkcja, która renderuje wiersze, jest przekazywana jako element podrzędny do funkcji FixedSizeList. Dostęp do szczegółowych informacji o konkretnym elemencie można uzyskać za pomocą argumentu index (items[index]).
  • Do metody renderowania wierszy jest też przekazywany parametr style, który musi być dołączony do elementu wiersza. Elementy listy są pozycjonowane bezwzględnie, a ich wartości wysokości i szerokości są przypisane w miejscu, co jest realizowane przez parametr style.

Wcześniej pokazany w tym artykule przykład zakłócenia to przykład komponentu FixedSizeList.

Kiedy używać list o zmiennym rozmiarze

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

Poniżej znajduje się przykład tego komponentu.

Funkcja rozmiaru produktu przekazana do atrybutu itemSize w tym przykładzie losuje wysokości wierszy. W rzeczywistych zastosowaniach powinna jednak istnieć logika definiująca rozmiary każdego produktu. Najlepiej, gdyby te rozmiary były obliczane na podstawie danych lub pochodziły z interfejsu API.

Siatka

react-window obsługuje też wirtualizację wielowymiarowych list lub siatek. W tym kontekście „okno” widocznych treści zmienia się, gdy użytkownik przewija ekran poziomo i pionowo.

Przesuwanie okna z treścią w wirtualnej siatce jest dwuwymiarowe
Przesuwanie „okna” z treściami w wirtualnej siatce jest dwuwymiarowe

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

  • W przypadku FixedSizeGrid interfejs API jest podobny, ale wysokość, szerokość i liczba elementów muszą być reprezentowane zarówno w kolumnach, jak i w wierszach.
  • W przypadku funkcji VariableSizeGrid szerokość kolumn i wysokość wierszy można zmieniać przez przekazanie funkcji zamiast wartości do odpowiednich właściwości.

Aby zobaczyć przykłady wirtualizowanych sieci, zapoznaj się z dokumentacją.

Leniwe ładowanie podczas przewijania

Wiele witryn poprawia wydajność, oczekując na wczytanie i renderowanie elementów z długiej listy, dopóki użytkownik nie przewinie strony w dół. Ta technika, powszechnie określana jako „nieskończone wczytywanie”, dodaje do listy nowe węzły DOM, gdy użytkownik przewija stronę poniżej określonego progu w pobliżu końca. Chociaż jest to lepsze niż wczytywanie wszystkich elementów na liście naraz, to nadal wypełnia DOM tysiącami wierszy, jeśli użytkownik przewinął tak wiele wierszy. Może to prowadzić do nadmiernego zwiększenia rozmiaru DOM, co zaczyna wpływać na wydajność, ponieważ spowalnia obliczenia stylów i mutacje DOM.

Oto podsumowanie tego diagramu:

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

Najlepszym sposobem na rozwiązanie tego problemu jest dalsze korzystanie z biblioteki takiej jak react-window, aby zachować małe „okno” elementów na stronie, ale też stosować ładowanie opóźnione nowych wpisów, gdy użytkownik przewija stronę w dół. Możliwe jest to dzięki osobnemu pakietowi react-window-infinite-loader, który jest dostępny w ramach react-window.

Zastanów się nad tym fragmentem kodu, który pokazuje stan zarządzany w rodzicielskim komponencie 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 podrzędnego elementu ListComponent, który zawiera nieskończoną listę wczytywania. Jest to ważne, ponieważ po przekroczeniu przez użytkownika określonego punktu ładowania nieskończony ładowar potrzebuje wywołać funkcję zwrotną, aby wczytać więcej elementów.

Oto jak może wyglądać element 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;

Tutaj komponent FixedSizeList jest ujęty w ramy komponentu InfiniteLoader. Elementy przypisane do ładowarki:

  • isItemLoaded: metoda sprawdzająca, czy określony element został załadowany
  • itemCount: liczba elementów na liście (lub oczekiwana)
  • loadMoreItems: funkcja wywołania zwracająca obietnicę, która zwraca dodatkowe dane dotyczące listy.

Właściwość render służy do zwracania funkcji, której komponent listy używa do renderowania. Atrybuty onItemsRenderedref to atrybuty, które muszą zostać przekazane.

Poniżej znajdziesz przykład działania nieskończonego wczytywania z wirtualizowaną listą.

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

Podczas sprawdzania pola index określonego elementu można dla niego pokazać inny stan wczytywania w zależności od tego, czy wysłano żądanie dla nowszych pozycji, a element wciąż się ładuje.

Na przykład:

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

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

Nadmiarowość obrazu

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

Aby zwiększyć wygodę użytkowników korzystających z list wirtualnych, funkcja react-window umożliwia skanowanie elementów z właściwością overscanCount. Pozwala to określić, ile elementów poza widocznym „oknem” ma zostać wyrenderowane w danym momencie.

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

Parametr overscanCount działa zarówno w przypadku komponentów FixedSizeList, jak i VariableSizeList. Jego domyślna wartość to 1. W zależności od tego, jak duża jest lista oraz od rozmiaru każdego elementu, nadmiarowe skanowanie więcej niż 1 wpisu może pomóc uniknąć wyraźnego pojawienia się pustej przestrzeni przy przewijaniu przez użytkownika. Zbyt duża liczba wpisów może jednak negatywnie wpłynąć na wydajność. Celem korzystania z listy wirtualnej jest zminimalizowanie liczby pozycji do tych, które użytkownik może zobaczyć w danym momencie, dlatego staraj się, aby liczba elementów skanowanych nadmiernie była jak najmniejsza.

W przypadku FixedSizeGridVariableSizeGrid użyj właściwości overscanColumnsCountoverscanRowsCount, aby kontrolować odpowiednio liczbę kolumn i wierszy do przeskanowania.

Podsumowanie

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

  1. Pomiar wydajności podczas renderowania i przewijania. Z tego artykułu dowiesz się, jak za pomocą miernika klatek na sekundę w Narzędziach deweloperskich w Chrome można sprawdzić wydajność renderowania elementów na liście.
  2. Uwzględnij react-window w przypadku 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 chcesz stosować opóźnione wczytywanie elementów podczas przewijania przez użytkownika, zwróć uwagę, aby kończyć wirtualizowaną listę elementem react-window-infinite-loader.
  5. Używaj właściwości overscanCount w przypadku list oraz właściwości overscanColumnsCount i overscanRowsCount w przypadku siatek, aby uniknąć mrugania pustej treści. Nie skanuj zbyt wielu pozycji, ponieważ może to negatywnie wpłynąć na wydajność.