---
layout: post
title: Виртуализация больших списков с помощью библиотеки react-window
subhead: Сверхбольшие таблицы и списки могут значительно снижать производительность сайта. В этом случае на помощь может прийти виртуализация.
hero: image/admin/CVkKShuaQw4CfZBg3Eub.jpg
date: 2019-04-29
description: react-window — это библиотека, с помощью которой можно эффективно отображать большие списки.
authors:
  - houssein
  - developit
feedback:
  - api
---

[`react-window`](https://react-window.now.sh/#/examples/list/fixed-size) — это библиотека, с помощью которой можно эффективно отображать большие списки.

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

{% Glitch { id: 'react-window-fixed', path: 'src/App.js', height: 750 } %}

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

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

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

<figure>{% Img src="image/tcFciHGuF3MxnTr1y5ue01OGLBn2/aWscOPGSFKVAIkgnUplQ.jpg", alt="Окно содержимого в виртуализированном списке", width="578", height="525" %} <figcaption> Перемещение «окна» содержимого в виртуализированном списке </figcaption></figure>

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

## Библиотека react-window

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

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

Используйте компонент `FixedSizeList` для длинных одномерных списков с элементами одинакового размера.

```jsx
import React from 'react';
import { FixedSizeList } from 'react-window';

const items = [...] // Список элементов

const Row = ({ index, style }) => (
  <div style={style}>
     {/* Определение компонента строки с использованием массива 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`.

{% Aside 'caution' %} Не назначайте свойства `height` и `width` списку или элементу списка с помощью внешнего файла CSS. Они будут проигнорированы, так как эти атрибуты стиля применяются в коде. {% endAside %}

В приведенном выше примере из Glitch показан компонент `FixedSizeList`.

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

Для отображения списков с элементами разного размера используйте компонент `VariableSizeList`. Он работает точно так же, как списки элементов фиксированного размера, но вместо определенного значения в свойство `itemSize` нужно передавать функцию.

```jsx
import React from 'react';
import { VariableSizeList } from 'react-window';

const items = [...] // Список элементов

const Row = ({ index, style }) => (
  <div style={style}>
     {/* Определение компонента строки с использованием массива items[index] */}
  </div>
);

const getItemSize = index => {
  // Возвращаем размер массива items[index]
}

const ListComponent = () => (
  <VariableSizeList
    height={500}
    width={500}
    itemCount={items.length}
    itemSize={getItemSize}
  >
    {Row}
  </VariableSizeList>
);

export default ListComponent;
```

Ниже показан пример этого компонента.

{% Glitch { id: 'react-window-variable', path: 'src/ListComponent.js', height: 750 } %}

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

{% Aside %} Компоненты `FixedSizeList` и `VariableSizeList` поддерживают горизонтальные списки при использовании свойства `layout="horizontal"`. Пример такого списка см. в [документации](https://react-window.now.sh/#/examples/list/fixed-size). {% endAside %}

### Таблицы

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

<figure>{% Img src="image/tcFciHGuF3MxnTr1y5ue01OGLBn2/1j2qoGW8bFzBNiOzaJKZ.jpg", alt="Перемещение окна содержимого в виртуализированной таблице в двух измерениях", width="739", height="516" %} <figcaption> Перемещение «окна» содержимого в виртуализированной таблице в двух измерениях </figcaption></figure>

В этом случае также можно использовать компоненты `FixedSizeGrid` и `VariableSizeGrid` (в зависимости от того, изменяется ли размер элементов списка).

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

Примеры виртуализированных таблиц см. в [документации](https://react-window.now.sh/#/examples/grid/fixed-size).

{% Aside %} Помимо базовых компонентов для создания эффективных списков и таблиц, библиотека `react-window` предоставляет и другие возможности, например функции прокрутки к определенному элементу или отображения индикатора, когда пользователь прокручивает список или таблицу. Соответствующие примеры см. в [документации](https://react-window.now.sh/#/examples/list/scrolling-indicators). {% endAside %}

## Отложенная загрузка при прокрутке

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

Ниже приведена схема со сводкой всего вышесказанного.

<figure>{% Img src="image/tcFciHGuF3MxnTr1y5ue01OGLBn2/dKuKVjP02xWxO9LPoOuc.jpg", alt="Разница при прокрутке обычного и виртуализированного списков", width="800", height="531" %} <figcaption> Разница при прокрутке обычного и виртуализированного списков </figcaption></figure>

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

Рассмотрим еще один фрагмент кода. В нем показан пример состояния, которым управляет родительский компонент `App`.

```jsx
import React, { Component } from 'react';

import ListComponent from './ListComponent';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      items: [], // Здесь мы создаем первоначальный список
      moreItemsLoading: false,
      hasNextPage: true
    };

    this.loadMore = this.loadMore.bind(this);
  }

  loadMore() {
   // Метод для получения новых записей списка
  }

  render() {
    const { items, moreItemsLoading, hasNextPage } = this.state;

    return (
      <ListComponent
        items={items}
        moreItemsLoading={moreItemsLoading}
        loadMore={this.loadMore}
        hasNextPage={hasNextPage}
      />
    );
  }
}

export default App;
```

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

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

```jsx
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 }) => (
     {/* Определяем компонент строки с использованием массива 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](https://reactjs.org/docs/render-props.html#using-props-other-than-render) используется для возврата функции, которую компонент списка использует для рендеринга. `onItemsRendered` и `ref` — это атрибуты, которые необходимо передать.

В примере ниже показано, как функция бесконечной загрузки работает с виртуализированным списком.

{% Glitch { id: 'react-window-infinite', path: 'src/ListComponent.js', height: 750 } %}

Ощущения пользователя при прокрутке списка могут не измениться, но теперь мы делаем запрос на получение 10 пользователей из [Random User API](https://randomuser.me/) каждый раз, когда пользователь прокручивает содержимое окна в сторону конца списка. При этом выполняется рендеринг не более одного «окна» результатов за раз.

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

Пример:

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

  if (itemLoading) {
      // Возвращаем информацию о состоянии загрузки
  } else {
      // Возвращаем элемент
  }
};
```

## Избыточное сканирование

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

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

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

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

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

## Вывод

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

1. Измерьте производительность рендеринга и прокрутки. В [этой статье](https://addyosmani.com/blog/react-window/) рассказывается, как с помощью [измерителя FPS](https://developer.chrome.com/docs/devtools/evaluate-performance/#analyze_frames_per_second) в Chrome DevTools оценить эффективность рендеринга элементов в списке.
2. Добавьте библиотеку `react-window` для работы с длинными списками или таблицами, снижающими производительность.
3. Если вам нужны функции, не поддерживаемые в библиотеке `react-window`, и вы не можете самостоятельно реализовать их, попробуйте использовать библиотеку [`react-virtualized`](https://github.com/bvaughn/react-virtualized).
4. Если нужно выполнять отложенную загрузку элементов по мере прокручивания списка пользователем, вложите виртуализированный список в элемент `react-window-infinite-loader`.
5. Используя свойство `overscanCount` для списков (или свойства `overscanColumnsCount` и `overscanRowsCount` для таблиц), можно предотвратить мигание пустого содержимого. Не выполняйте избыточное сканирование для слишком большого количества записей, так как это снизит производительность.
