Виртуализируйте большие списки с помощью окна реагирования

Сверхбольшие таблицы и списки могут значительно снизить производительность вашего сайта. Виртуализация может помочь!

react-window — это библиотека, которая позволяет эффективно отображать большие списки.

Вот пример списка, содержащего 1000 строк, отображаемого с помощью react-window . Попробуйте прокручивать как можно быстрее.

Почему это полезно?

Бывают случаи, когда вам нужно отобразить большую таблицу или список, содержащий много строк. Загрузка каждого элемента такого списка может существенно повлиять на производительность.

Виртуализация списков , или «оконность», — это концепция рендеринга только того, что видно пользователю. Количество элементов, отображаемых сначала, представляет собой очень небольшую часть всего списка, и «окно» видимого контента перемещается, когда пользователь продолжает прокручивать. Это улучшает производительность рендеринга и прокрутки списка.

Окно контента в виртуализированном списке
Перемещение «окна» контента в виртуализированном списке

Узлы DOM, выходящие из «окна», перерабатываются или немедленно заменяются более новыми элементами, когда пользователь прокручивает список вниз. Это сохраняет количество всех отображаемых элементов в зависимости от размера окна.

окно реакции

react-window — это небольшая сторонняя библиотека, которая упрощает создание виртуализированных списков в вашем приложении. Он предоставляет ряд базовых API, которые можно использовать для различных типов списков и таблиц.

Когда использовать списки фиксированного размера

Используйте 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;
  • FixedSizeList принимает свойства height , width и itemSize для управления размером элементов в списке.
  • Функция, которая визуализирует строки, передается как дочерняя функция в методе FixedSizeList . Подробную информацию о конкретном элементе можно получить с помощью аргумента index ( items[index] ).
  • Параметр style также передается в метод рендеринга строки, который должен быть прикреплен к элементу строки. Элементы списка позиционируются абсолютно, а их значения высоты и ширины назначаются встроенными, и за это отвечает параметр style .

Пример Glitch, показанный ранее в этой статье, показывает пример компонента FixedSizeList .

Когда использовать списки переменного размера

Используйте компонент VariableSizeList для отображения списка элементов разных размеров. Этот компонент работает так же, как список фиксированного размера, но вместо этого ожидает функцию для свойства 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;

На следующей вставке показан пример этого компонента.

Функция размера элемента, переданная в свойство itemSize , в этом примере рандомизирует высоту строк. Однако в реальном приложении должна быть реальная логика, определяющая размеры каждого элемента. В идеале эти размеры должны быть рассчитаны на основе данных или получены из API.

Сетки

react-window также обеспечивает поддержку виртуализации многомерных списков или сеток. В этом контексте «окно» видимого контента меняется при прокрутке пользователем по горизонтали и вертикали.

Перемещение окна контента в виртуализированной сетке является двухмерным.
Перемещение «окна» контента в виртуализированной сетке является двухмерным.

Аналогичным образом можно использовать оба компонента: FixedSizeGrid и VariableSizeGrid в зависимости от того, может ли размер конкретных элементов списка меняться.

  • Для FixedSizeGrid API примерно такой же, но с учетом того, что высоты, ширины и количество элементов должны быть представлены как для столбцов, так и для строк.
  • Для VariableSizeGrid ширину столбца и высоту строки можно изменить, передав функции вместо значений в соответствующие реквизиты.

Посмотрите документацию , чтобы увидеть примеры виртуализированных сеток.

Ленивая загрузка при прокрутке

Многие веб-сайты повышают производительность, ожидая загрузки и отображения элементов в длинном списке до тех пор, пока пользователь не прокрутит их вниз. Этот метод, обычно называемый «бесконечная загрузка», добавляет в список новые узлы DOM, когда пользователь прокручивает определенный порог ближе к концу. Хотя это лучше, чем загружать все элементы списка одновременно, в конечном итоге DOM будет заполнен тысячами записей строк, если пользователь прокрутит их дальше. Это может привести к чрезмерно большому размеру DOM, что начинает влиять на производительность, замедляя вычисления стилей и изменения DOM.

Следующая диаграмма может помочь обобщить это:

Разница в прокрутке обычного и виртуализированного списка
Разница в прокрутке обычного и виртуализированного списка

Лучший подход к решению этой проблемы — продолжать использовать такую ​​библиотеку, как react-window для поддержания небольшого «окна» элементов на странице, а также ленивой загрузки новых записей по мере прокрутки пользователем вниз. Отдельный пакет react-window-infinite-loader делает это возможным с помощью react-window .

Рассмотрим следующий фрагмент кода, который показывает пример состояния, управляемого в родительском компоненте 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;

Метод loadMore передается в дочерний ListComponent , который содержит список бесконечного загрузчика. Это важно, поскольку бесконечный загрузчик должен запустить обратный вызов для загрузки большего количества элементов, как только пользователь прокрутит определенную точку.

Вот как может выглядеть ListComponent , отображающий список:

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;

Здесь FixedSizeList заключен в InfiniteLoader . Загрузчику назначены следующие реквизиты:

  • isItemLoaded : метод, который проверяет, загружен ли определенный элемент.
  • itemCount : количество элементов в списке (или ожидаемое)
  • loadMoreItems : обратный вызов, который возвращает обещание, которое разрешается в дополнительные данные для списка.

Свойство рендеринга используется для возврата функции, которую компонент списка использует для рендеринга. Атрибуты onItemsRendered и ref — это атрибуты, которые необходимо передать.

Ниже приведен пример того, как бесконечная загрузка может работать с виртуализированным списком.

Прокрутка списка вниз может выглядеть так же, но теперь выполняется запрос на получение 10 пользователей из API случайных пользователей каждый раз, когда вы прокручиваете список близко к концу. Все это делается при одновременном рендеринге только одного «окна» результатов.

Проверяя index определенного элемента, для элемента можно отобразить различное состояние загрузки в зависимости от того, был ли сделан запрос на новые записи, и элемент все еще загружается.

Например:

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

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

Оверсканирование

Поскольку элементы в виртуализированном списке изменяются только при прокрутке пользователем, пустое пространство может ненадолго мигнуть, поскольку вот-вот отобразятся новые записи. Вы можете попробовать быстро прокрутить любой из предыдущих примеров в этом руководстве, чтобы заметить это.

Чтобы улучшить взаимодействие с пользователем при работе с виртуализированными списками, react-window позволяет вам пересканировать элементы с помощью свойства overscanCount . Это позволяет вам определить, сколько элементов за пределами видимого «окна» нужно отображать постоянно.

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

overscanCount работает как для компонентов FixedSizeList , так и для VariableSizeList , и имеет значение по умолчанию, равное 1. В зависимости от размера списка, а также размера каждого элемента, сверхсканирование более чем одной записи может помочь предотвратить заметное появление пустого пространства при пользователь прокручивает. Однако чрезмерное сканирование слишком большого количества записей может отрицательно повлиять на производительность. Весь смысл использования виртуализированного списка состоит в том, чтобы свести к минимуму количество записей до того уровня, который пользователь может видеть в любой момент, поэтому старайтесь свести количество пересканированных элементов к минимуму.

Для FixedSizeGrid и VariableSizeGrid используйте свойства overscanColumnsCount и overscanRowsCount , чтобы контролировать количество столбцов и строк для сверхсканирования соответственно.

Заключение

Если вы не знаете, с чего начать виртуализацию списков и таблиц в вашем приложении, выполните следующие действия:

  1. Измерьте производительность рендеринга и прокрутки. В этой статье показано, как можно использовать счетчик FPS в Chrome DevTools для определения эффективности отображения элементов в списке.
  2. Включите react-window для любых длинных списков или сеток, влияющих на производительность.
  3. Если какие-то функции не поддерживаются в react-window , рассмотрите возможность использования react-virtualized если вы не можете добавить эту функциональность самостоятельно.
  4. Оберните свой виртуальный список с помощью react-window-infinite-loader если вам нужно отложенно загружать элементы при прокрутке пользователем.
  5. Используйте свойство overscanCount для своих списков и свойства overscanColumnsCount и overscanRowsCount для своих сеток, чтобы предотвратить появление пустого содержимого. Не сканируйте слишком много записей, так как это отрицательно скажется на производительности.