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

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

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

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

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

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

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

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

react-window

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 .

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

Компонент 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 : Функция обратного вызова, возвращающая промис, который разрешается в добавление дополнительных данных в список.

Свойство render используется для возврата функции, которую компонент списка использует для рендеринга. Атрибуты 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 для оценки эффективности отрисовки элементов в списке.
  2. Добавьте react-window для длинных списков или сеток, которые влияют на производительность.
  3. Если в react-window отсутствуют некоторые функции, рассмотрите возможность использования react-virtualized если вы не можете добавить эту функциональность самостоятельно.
  4. Если вам нужно отложенно загружать элементы по мере прокрутки пользователем виртуализированного списка, оберните его в react-window-infinite-loader .
  5. Используйте свойство overscanCount для списков и свойства overscanColumnsCount и overscanRowsCount для таблиц, чтобы предотвратить появление пустого содержимого. Не пересканируйте слишком много элементов, так как это негативно скажется на производительности.