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.

Oto przykład listy zawierającej 1000 wierszy, która jest renderowana za pomocą elementu react-window. Spróbuj przewijać jak najszybciej.

Dlaczego ta funkcja jest przydatna?

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

Wirtualizacja listy, czyli „okienkowanie”, to koncepcja renderowania tylko tego, co jest widoczne dla użytkownika. Liczba elementów renderowanych na początku jest bardzo małym podzbiorem całej listy, a „okno” widocznych treści przesuwa się, gdy użytkownik przewija dalej. Poprawia to zarówno renderowanie, jak i przewijanie listy.

Okno treści na liście wirtualnej
Przesuwanie „okna” treści na wirtualnej liście

Węzły DOM, które opuszczają „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 wirtualnych list w aplikacji. Udostępnia on wiele 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

Użyj komponentu FixedSizeList, jeśli masz długą, jednowymiarową listę elementów o jednakowych rozmiarach.

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 kontrolują rozmiar elementów na liście.
  • Funkcja renderująca wiersze jest przekazywana jako element podrzędny do komponentu FixedSizeList. Szczegóły dotyczące 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 wartości ich wysokości i szerokości są przypisywane w wierszu. Odpowiada za to parametr style.

Przykład Glitch przedstawiony wcześniej w tym artykule pokazuje komponent FixedSizeList.

Kiedy używać list o zmiennej wielkości

Użyj komponentu VariableSizeList, aby wyrenderować 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 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;

Poniższy kod do umieszczenia pokazuje przykład tego komponentu.

Funkcja rozmiaru elementu przekazana do właściwości itemSize losuje w tym przykładzie wysokości wierszy. 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 uzyskiwane z interfejsu API.

Siatki

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

Przesuwne okno treści w zwirtualizowanej siatce jest dwuwymiarowe
Przesuwanie „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 rozmiar poszczególnych elementów listy może się różnić.

  • W przypadku FixedSizeGrid interfejs API jest podobny, ale wysokości, szerokości i liczby elementów muszą być reprezentowane zarówno w przypadku kolumn, jak i wierszy.
  • W przypadku komponentu VariableSizeGrid można zmienić zarówno szerokość kolumn, jak i wysokość wierszy, przekazując funkcje zamiast wartości do odpowiednich właściwości.

Przykłady zwirtualizowanych siatek znajdziesz w dokumentacji.

Leniwe ładowanie podczas przewijania

Wiele witryn poprawia wydajność, czekając z wczytaniem i wyrenderowaniem elementów na długiej liście, aż użytkownik przewinie stronę w dół. Ta technika, powszechnie określana jako „nieskończone wczytywanie”, dodaje nowe węzły DOM do listy, gdy użytkownik przewija ją do określonego progu blisko końca. Chociaż jest to lepsze rozwiązanie niż wczytywanie wszystkich elementów na liście naraz, w przypadku przewinięcia przez użytkownika dużej liczby wierszy w DOM-ie nadal może się pojawić wiele tysięcy wpisów. Może to prowadzić do nadmiernie dużego rozmiaru DOM, co zaczyna wpływać na wydajność, ponieważ obliczenia stylów i mutacje DOM są wolniejsze.

Może Ci w tym pomóc poniższy diagram:

Różnica w przewijaniu między zwykłą a zwirtualizowaną listą
Różnica w przewijaniu zwykłej i wirtualnej listy

Najlepszym rozwiązaniem tego problemu jest dalsze korzystanie z biblioteki takiej jak react-window, aby utrzymywać małe „okno” elementów na stronie, ale także leniwe wczytywanie nowszych wpisów podczas przewijania strony w dół. Umożliwia to osobny pakiet react-window-infinite-loader, który zawiera react-window.

Przyjrzyj się poniższemu fragmentowi 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;

Do komponentu podrzędnego ListComponent, który zawiera listę nieskończonego ładowania, przekazywana jest loadMore metoda. Jest to ważne, ponieważ po przewinięciu strony do określonego miejsca nieskończony moduł wczytujący musi 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;

W tym przypadku komponent FixedSizeList jest zawarty w InfiniteLoader. Do modułu wczytującego przypisane są te właściwości:

  • isItemLoaded: metoda sprawdzająca, czy dany element został wczytany
  • itemCount: liczba elementów na liście (lub oczekiwana liczba elementów)
  • loadMoreItems: funkcja zwrotna, która zwraca obietnicę, która jest rozwiązywana w postaci dodatkowych danych dla listy.

Render prop służy do zwracania funkcji, której komponent listy używa do renderowania. Atrybuty onItemsRenderedref muszą być przekazywane.

Poniżej znajdziesz przykład działania nieskończonego ładowania w przypadku listy wirtualizowanej.

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

Po kliknięciu index przy określonym elemencie może się wyświetlić inny stan ładowania elementu w zależności od tego, czy wysłano prośbę o nowe wpisy i czy element nadal 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 liście wirtualizowanej zmieniają się tylko wtedy, gdy użytkownik przewija ekran, więc puste miejsce może na chwilę błysnąć, gdy mają się pojawić nowsze wpisy. Możesz to sprawdzić, szybko przewijając dowolny z poprzednich przykładów w tym przewodniku.

Aby zwiększyć wygodę użytkowników korzystających z list wirtualizowanych, react-window umożliwia przeszukiwanie elementów za pomocą właściwości overscanCount. Dzięki temu możesz określić, ile elementów poza widocznym „oknem” ma być zawsze renderowanych.

<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 wielkości listy i rozmiaru poszczególnych elementów nadmierne skanowanie więcej niż 1 pozycji może zapobiec zauważalnemu błyskowi pustej przestrzeni podczas przewijania przez użytkownika. Jednak skanowanie zbyt wielu wpisów może negatywnie wpłynąć na wydajność. Cały sens korzystania z wirtualizowanej listy polega na zminimalizowaniu liczby pozycji do tych, które użytkownik może zobaczyć w danym momencie, więc staraj się, aby liczba elementów, które zostały przeskanowane, była jak najmniejsza.

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

Podsumowanie

Jeśli nie wiesz, 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ą licznika FPS w Narzędziach deweloperskich w Chrome sprawdzać, jak wydajnie elementy są renderowane na liście.
  2. Dodaj atrybut react-window do wszystkich długich list lub siatek, które wpływają na wydajność.
  3. Jeśli w react-window nie ma niektórych funkcji, rozważ użycie react-virtualized, jeśli nie możesz samodzielnie dodać tej funkcji.
  4. Jeśli chcesz leniwie wczytywać elementy podczas przewijania przez użytkownika, umieść wirtualizowaną listę w tagach react-window-infinite-loader.
  5. Użyj właściwości overscanCount w przypadku list, a właściwości overscanColumnsCountoverscanRowsCount w przypadku siatek, aby zapobiec wyświetlaniu pustych treści. Nie skanuj zbyt wielu wpisów, ponieważ negatywnie wpłynie to na wydajność.