Виртуализация больших списков с помощью библиотеки react-window
Сверхбольшие таблицы и списки могут значительно снижать производительность сайта. В этом случае на помощь может прийти виртуализация.
react-window
— это библиотека, с помощью которой можно эффективно отображать большие списки.
Вот пример списка, в котором 1000 строк, отображаемых с помощью библиотеки react-window
. Попробуйте прокрутить этот список как можно быстрее.
Почему это полезно? #
Иногда требуется отобразить большую таблицу или список с большим количеством строк. Загрузив все элементы такого списка, можно сильно ухудшить производительность страницы.
Виртуализация списков или «кадрирование» — это концепция, согласно которой отображают только те элементы, которые видны пользователю. Сначала отображают очень небольшое количество элементов списка. Когда пользователь прокручивает список, «окно» видимого содержимого перемещается. Это повышает производительность списка как при его отображении, так и при прокрутке.
Когда пользователь прокручивает список вниз, узлы DOM, которые выходят за пределы «окна», повторно используются или немедленно заменяются новыми элементами. Это позволяет поддерживать постоянное количество отображаемых элементов, зависящее от размера окна.
Библиотека react-window #
react-window
— это небольшая сторонняя библиотека, упрощающая создание виртуализированных списков в приложении. Имеющиеся в ней базовые API можно использовать для работы со списками и таблицами различных типов.
Когда использовать списки фиксированного размера #
Используйте компонент FixedSizeList
для длинных одномерных списков с элементами одинакового размера.
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
.
В приведенном выше примере из Glitch показан компонент FixedSizeList
.
Когда использовать списки переменного размера #
Для отображения списков с элементами разного размера используйте компонент VariableSizeList
. Он работает точно так же, как списки элементов фиксированного размера, но вместо определенного значения в свойство itemSize
нужно передавать функцию.
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;
Ниже показан пример этого компонента.
В этом примере функция размера элемента, передаваемая в свойство 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: [], // Здесь мы создаем первоначальный список
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
, выполняющий рендеринг списка:
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 используется для возврата функции, которую компонент списка использует для рендеринга. onItemsRendered
и ref
— это атрибуты, которые необходимо передать.
В примере ниже показано, как функция бесконечной загрузки работает с виртуализированным списком.
Ощущения пользователя при прокрутке списка могут не измениться, но теперь мы делаем запрос на получение 10 пользователей из Random User API каждый раз, когда пользователь прокручивает содержимое окна в сторону конца списка. При этом выполняется рендеринг не более одного «окна» результатов за раз.
Проверяя значение index
определенного элемента, можно отображать другое состояние загрузки для элемента в зависимости от того, был ли сделан запрос на получение новых записей и загружается ли все еще элемент.
Пример:
const Row = ({ index, style }) => {
const itemLoading = index === items.length;
if (itemLoading) {
// Возвращаем информацию о состоянии загрузки
} else {
// Возвращаем элемент
}
};
Избыточное сканирование #
Так как элементы в виртуализированном списке сменяются, только когда пользователь прокручивает список, перед отображением новых записей на короткое время может отображаться пустое пространство. Чтобы увидеть это, попробуйте быстро прокрутить любой список в предыдущих примерах в данном руководстве.
Чтобы повысить удобство для пользователей, работающих с виртуализированными списками, библиотека react-window
позволяет выполнять избыточное сканирование элементов с использованием свойства overscanCount
. С помощью этого свойства можно указать, какое количество элементов, находящихся за пределами видимого «окна», нужно всегда отображать.
<FixedSizeList
//...
overscanCount={4}
>
{...}
</FixedSizeList>
Свойство overscanCount
работает как для компонента FixedSizeList
, так и для компонента VariableSizeList
; по умолчанию оно имеет значение 1. В зависимости от величины списка и размера каждого элемента, выполняя избыточное сканирование нескольких записей, можно предотвратить заметное мигание пустого пространства, когда пользователь прокручивает список. Однако избыточное сканирование слишком большого количества записей может снизить производительность. Весь смысл виртуализированного списка в том, чтобы использовать только то количество записей, которое можно отображать для пользователя в тот или ной момент времени, поэтому старайтесь выполнять избыточное сканирование для минимально возможного количества элементов.
В компонентах FixedSizeGrid
и VariableSizeGrid
можно управлять количеством столбцов и строк, для которых необходимо выполнять избыточное сканирование. Для этого используют свойства overscanColumnsCount
и overscanRowsCount
соответственно.
Вывод #
Если вы не знаете, с чего начать виртуализацию списков и таблиц в приложении, выполните указанные ниже действия.
- Измерьте производительность рендеринга и прокрутки. В этой статье рассказывается, как с помощью измерителя FPS в Chrome DevTools оценить эффективность рендеринга элементов в списке.
- Добавьте библиотеку
react-window
для работы с длинными списками или таблицами, снижающими производительность. - Если вам нужны функции, не поддерживаемые в библиотеке
react-window
, и вы не можете самостоятельно реализовать их, попробуйте использовать библиотекуreact-virtualized
. - Если нужно выполнять отложенную загрузку элементов по мере прокручивания списка пользователем, вложите виртуализированный список в элемент
react-window-infinite-loader
. - Используя свойство
overscanCount
для списков (или свойстваoverscanColumnsCount
иoverscanRowsCount
для таблиц), можно предотвратить мигание пустого содержимого. Не выполняйте избыточное сканирование для слишком большого количества записей, так как это снизит производительность.