Wirtualizacja dużych list za pomocą okna reakcji

Bardzo duże tabele i listy mogą znacznie spowalniać działanie witryny. Może pomóc wirtualizacja.

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

Oto przykład listy zawierającej 1000 wierszy renderowanych za pomocą funkcji react-window. Spróbuj przewinąć stronę tak szybko, jak to możliwe.

Dlaczego to takie przydatne?

Może się zdarzyć, że musisz 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 lub „okno” oznacza renderowanie tylko tego, co jest widoczne dla użytkownika. Liczba elementów renderowanych na początku stanowi bardzo mały podzbiór całej listy, a „okno” widocznej zawartości przesuwa się, gdy użytkownik kontynuuje przewijanie. Poprawia to zarówno wydajność renderowania, jak i przewijania listy.

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

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

okno reakcji

react-window to mała biblioteka innej firmy, która ułatwia tworzenie wirtualnych list w aplikacji. Udostępnia on szereg podstawowych interfejsów API, których można używać z różnymi rodzajami list i tabel.

Kiedy używać list o stałym rozmiarze

Jeśli masz długą, jednowymiarową listę elementów o jednakowych rozmiarach, 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, aby kontrolować rozmiar elementów na liście.
  • Funkcja, która renderuje wiersze, jest przekazywana do funkcji FixedSizeList jako element podrzędny. Aby uzyskać dostęp do informacji o konkretnym elemencie, użyj argumentu index (items[index]).
  • Parametr style jest też przekazywany do metody renderowania wierszy, która musi być dołączony do elementu wiersza. Elementy list są umieszczone bezwzględnie, a ich wartości wysokości i szerokości są przypisane w tekście, za co odpowiada parametr style.

We wcześniejszym artykule o tym artykule znajdziesz przykład komponentu FixedSizeList.

Kiedy używać list o zmiennej rozmiaru

Aby wygenerować listę elementów o różnych rozmiarach, użyj komponentu VariableSizeList. Ten komponent działa tak samo jak lista o stałym rozmiarze, ale oczekuje funkcji właściwości itemSize zamiast konkretnej wartości.

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;

Umieszczony komponent to przykładowy element.

Funkcja rozmiaru elementu przekazywana do funkcji itemSize losowo określa wysokość wierszy w tym przykładzie. Jednak w praktyce powinna istnieć logika definiująca rozmiar każdego elementu. W miarę możliwości rozmiary powinny być obliczane na podstawie danych lub pobierane z interfejsu API.

Siatki

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

Przenoszenie okna treści w zwirtualizowanej siatce jest dwuwymiarowe.
Przenoszenie „okna” treści w zwirtualizowanej siatce jest dwuwymiarowe

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

  • W przypadku FixedSizeGrid interfejs API jest mniej więcej taki sam, ale w obu kolumnach i wierszach trzeba podać wysokość, szerokość i liczbę elementów.
  • W przypadku funkcji VariableSizeGrid szerokość kolumn i wysokość wierszy można zmienić, przekazując do nich funkcje zamiast wartości.

Przykłady zwirtualizowanych siatek znajdziesz w dokumentacji.

Leniwe ładowanie podczas przewijania

Wiele witryn podnosi wydajność, czekając na wczytanie i renderowanie elementów na długiej liście, aż użytkownik przewinie stronę w dół. Ta technika, często określana jako „nieskończone ładowanie”, dodaje do listy nowe węzły DOM, gdy użytkownik przewija stronę blisko określonego progu. To lepsze rozwiązanie niż ładowanie wszystkich elementów z listy od razu, ale w przypadku przewinięcia bloka DOM umieszcza w tym przypadku tysiące wierszy. Może to prowadzić do zbyt dużego rozmiaru DOM, który zaczyna wpływać na wydajność przez spowalnianie obliczeń stylu i mutacji DOM.

Ten diagram może pomóc w podsumowaniu informacji:

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

Najlepszym sposobem rozwiązania tego problemu jest dalsze korzystanie z biblioteki takiej jak react-window, aby zachować małe „okno” elementów na stronie i leniwe ładowanie nowszych wpisów, gdy użytkownik przewija stronę w dół. Oddzielny pakiet, react-window-infinite-loader, umożliwia to react-window.

Przyjrzyjmy się fragmentowi kodu poniżej, który pokazuje przykład stanu zarządzanego w nadrzędnym 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 listę nieskończonych modułów ładowania. Jest to ważne, ponieważ nieskończone wczytywanie musi uruchomić wywołanie zwrotne, aby wczytać więcej elementów, gdy użytkownik przewinie widok poza określony punkt.

Przykładowa funkcja ListComponent, która renderuje listę, może wyglądać tak:

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 zawarty w komponencie InfiniteLoader. Obiekty przypisane do ładowania:

  • isItemLoaded: metoda sprawdzania, czy określony element został wczytany.
  • itemCount: liczba produktów na liście (lub oczekiwanej).
  • loadMoreItems: wywołanie zwrotne, które zwraca obietnicę, która prowadzi do dodatkowych danych z listy.

Prop. renderowania służy do zwrócenia funkcji, której komponent listy używa do renderowania. Przekazujemy zarówno atrybuty onItemsRendered, jak i ref.

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

Przewijanie listy może wydawać się podobne, ale za każdym razem, gdy przewijasz do końca listy, następuje żądanie pobrania 10 użytkowników z interfejsu API losowego użytkownika. Wszystko to dzieje się podczas renderowania tylko jednego „okna” wyników naraz.

Podczas sprawdzania index danego elementu może pokazać się inny stan wczytywania elementu w zależności od tego, czy wysłano żądanie dotyczące nowszych wpisów, a element nadal się wczytuje.

Na przykład:

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

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

Nadmiarowe skanowanie

Elementy na zwirtualizowanej liście zmieniają się tylko wtedy, gdy użytkownik przewija stronę, dlatego puste miejsce może na chwilę migać, gdy zbliżają się nowe pozycje. Aby to zauważyć, możesz szybko przewinąć dowolny z poprzednich przykładów w tym przewodniku.

Aby zwiększyć wygodę użytkowników zwirtualizowanych list, react-window umożliwia nadmiarowe skanowanie elementów za pomocą właściwości overscanCount. Pozwala to określić, ile elementów poza widocznym „oknem” ma być stale renderowane.

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

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, a także od rozmiaru każdego elementu, nadmierne skanowanie więcej niż 1 wpisu może zapobiec zauważalnemu błyskowi pustej przestrzeni podczas przewijania. Jednak nadmierne skanowanie zbyt wielu wpisów może negatywnie wpłynąć na wydajność. Korzystanie z wirtualizowanej listy polega na zminimalizowaniu liczby wpisów do poziomu, który użytkownik może zobaczyć w danej chwili. Dlatego staraj się więc ograniczyć liczbę przeskanowanych elementów do jak najmniejszej liczby.

W przypadku FixedSizeGrid i VariableSizeGrid użyj właściwości overscanColumnsCount i overscanRowsCount, aby określić liczbę kolumn i wierszy, które mają być nadmiarowe.

Podsumowanie

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

  1. Mierz wydajność renderowania i przewijania. Z tego artykułu dowiesz się, jak za pomocą miernika klatek na sekundę dostępnego w Narzędziach deweloperskich w Chrome można sprawdzić wydajność renderowania elementów na liście.
  2. W przypadku długich list lub siatki, które wpływają na wydajność, uwzględnij atrybut react-window.
  3. Jeśli nie jesteś w stanie samodzielnie dodać niektórych funkcji do react-window, rozważ użycie react-virtualized.
  4. Jeśli potrzebujesz leniwego ładowania elementów, gdy użytkownik przewija stronę, dodaj do zwirtualizowanej listy element react-window-infinite-loader.
  5. Używaj w przypadku list właściwości overscanCount, a w siatkach – właściwości overscanColumnsCount i overscanRowsCount, aby uniknąć błysku pustej zawartości. Nie skanuj zbyt wielu wpisów, ponieważ wpłynie to negatywnie na wydajność.