Виртуальная прокрутка в React: реализация с нуля и использование react-window
Сколько раз вы сталкивались с ситуацией, когда в веб-приложении необходимо вывести список элементов? Это очень частое явление при создании современных веб-приложений. Проблема возникает, когда нужно вывести очень большой набор данных, скажем, 100 000 или более единиц одновременно, да еще и без пагинации. Это приведет к загрязнению DOM и займет много памяти браузера, что приведет к проблемам с производительностью и снижению качества обслуживания пользователя.
Виртуальная прокрутка - это одна из техник, которая может спасти вас в этой ситуации, поскольку отображаются только те элементы, которые пользователь действительно хочет увидеть, а остальные элементы будут загружаться виртуально по мере прокрутки к ним, вместо того чтобы отображать все сразу.
В этой статье мы рассмотрим, что такое виртуальная прокрутка и как можно реализовать виртуальную прокрутку в приложении React для вывода списка из 100 000 элементов без пагинации.
Что такое виртуальная прокрутка?
Как мы уже говорили, отображение огромного списка данных в DOM без пагинации может замедлить работу приложения из-за загрязнения DOM, потребления слишком большого объема памяти браузера, и, кроме того, конечный пользователь не будет использовать все данные сразу, он увидит только часть этих данных, а затем прокрутит список, чтобы увидеть больше данных.
Виртуальная прокрутка или windowing - это техника разработки веб-приложений, при которой отображаются только те элементы, которые видны в области просмотра, а остальные элементы виртуализируются с помощью верхней и нижней подкладки. По мере прокрутки списка старые элементы будут уничтожаться, а новые отображаться в зависимости от положения полосы прокрутки. Таким образом, мы не будем слишком нагружать DOM, отображая всё сразу. Использование этой техники в правильном сценарии может значительно повысить производительность приложения.
Нужна ли нам виртуализация?
Теперь, когда мы знаем, что такое виртуальная прокрутка, возникает вопрос: "А всегда ли нам нужна виртуальная прокрутка?".
Ответ на этот вопрос должен быть вполне очевиден: если вы создаете веб-приложение, в котором необходимо отображать огромный список элементов без пагинации, и вам действительно важна производительность, то, безусловно, виртуальная прокрутка - это то, что вы можете использовать.
Несмотря на то, что это хороший способ повысить производительность, есть несколько сценариев, в которых виртуальная прокрутка может и не понадобиться:
- Если список состоит менее чем из 50 или 100 данных. Если список состоит из небольшого количества элементов, например 50 или 100, виртуальная прокрутка может быть просто накладной и не окажет существенного влияния на производительность приложения.
- При отображении пагинационного списка. При отображении списка с некоторой пагинацией, очевидно, виртуальная прокрутка может оказаться бесполезной, поскольку весь смысл виртуальной прокрутки заключается в эффективном отображении элементов списка, когда пагинация не реализована.
Виртуализация в приложении React с нуля
Теперь, когда мы имеем некоторое представление о виртуальной прокрутке и о том, когда ее следует использовать, посмотрим, как можно реализовать виртуальную прокрутку в react-приложении без использования каких-либо сторонних инструментов.
Чтобы приступить к реализации этого подхода, сделаем несколько предположений:
- Высота каждой строки списка будет фиксированной.
- Общее количество элементов, выводимых в списке, известно заранее.
Если условия выполнены, создадим новое приложение React и построим компонент VirtualScroll
.
import { useState } from "react";
const VirtualScroll = ({
rowHeight,
totalItems,
items,
visibleItemsLength,
containerHeight,
}) => {
// Calculate the total height of the container
const totalHeight = rowHeight * totalItems;
// Current scroll position of the container
const [scrollTop, setScrollTop] = useState(0);
// Get the first element to be displayed
const startNodeElem = Math.ceil(scrollTop / rowHeight);
// Get the items to be displayed
const visibleItems = items?.slice(
startNodeElem,
startNodeElem + visibleItemsLength
);
// Add padding to the empty space
const offsetY = startNodeElem * rowHeight;
const handleScroll = (e) => {
// set scrollTop to the current scroll position of the container.
setScrollTop(e?.currentTarget?.scrollTop);
};
return (
<div
style={{
height: containerHeight,
overflow: "auto",
border: "5px solid black",
}}
onScroll={handleScroll}
>
<div style={{ height: totalHeight }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems}
</div>
</div>
</div>
);
};
export default VirtualScroll;
В приведенном выше коде мы выполняем очень простые математические действия: сначала вычисляем общую высоту контейнера totalHeight
путем умножения высоты одной строки rowHeight
на общее количество элементов, подлежащих отображению totalItems
.
Затем мы делим позицию прокрутки контейнера сверху scrollTop
на высоту одной строки rowHeight
, чтобы получить начальный элемент узла startNodeElem
, который будет отображаться в списке, мы также используем Math.ceil
для округления значения.
Затем мы нарезаем totalItems
, чтобы показать только элементы, начиная с startNodeElem
и заканчивая startNodeElem + visibleItemsLength
. Так, допустим, наш startNodeElem = 5
, тогда мы будем показывать элементы с 5-й позиции до 5 + visibleItemsLength
, считая, что visibleItemsLength
в данном случае равна 20. Таким образом, наш компонент будет отображать элементы, начиная с 5-й позиции и заканчивая 5 + 20 = 25
позициями.
Теперь каждый раз, когда пользователь прокручивает содержимое, значение scrollTop
будет пересчитываться, а startNodeElem
будет обновляться в зависимости от положения прокрутки, и мы будем смещать узлы вниз с помощью свойства transform: translateY
.
Теперь мы можем использовать наш компонент VirtualScroll
для отображения списка элементов.
import React from "react";
import VirtualScroll from "./virtual-scroll";
// Total items to be rendered
const totalItems = 100000;
const items = new Array(totalItems).fill(null).map((_, index) => {
return (
// Height of the div should be the same as what we are sending in rowHeight
<div style={{ height: "70px" }} key={index}>
<p>Row Number - {index}</p>
</div>
);
});
function App() {
return (
<>
<h1>Virtual Scroll From Scratch</h1>
<VirtualScroll
rowHeight={70}
totalItems={totalItems}
containerHeight="500px"
items={items}
visibleItemsLength={20}
/>
</>
);
}
export default App;
Здесь мы выводим список из 100000 элементов и устанавливаем значение containerHeight
равным 500px, а высоту каждой строки или rowHeight
равным 70px.
Теперь запустите свое приложение, чтобы увидеть виртуальную прокрутку в действии.
Как видите, при прокрутке списка мы не накапливаем лишние узлы, а уничтожаем предыдущие узлы и отрисовываем новые, основываясь на положении контейнера при прокрутке.
Вот вывод песочницы с приведённой выше реализацией: https://pvg2cx.csb.app/.
Вы можете заметить некоторое белое пространство при прокрутке, это связано с тем, что при каждом прокручивании контейнера происходит манипуляция со всем DOM, и мы заново все вычисляем.
Таким образом, это была очень наивная реализация виртуального скроллинга с нуля в react-приложении, которая, конечно, может быть оптимизирована для достижения лучших результатов.
Виртуальная прокрутка в React с использованием react-window
Как мы видели в предыдущем разделе, мы можем реализовать виртуальную прокрутку с нуля. В этом разделе мы рассмотрим сторонний инструмент react-window, который позволяет реализовать виртуальный скроллинг гораздо проще.
react-window
- это очень легкий пакет React, который позволяет выводить виртуализированный список, избавляя вас от лишней работы, которая может потребоваться при реализации решения с нуля.
Чтобы начать работу с react-window, нам сначала нужно установить его в наше react-приложение.
# Yarn
yarn add react-window
# NPM
npm install --save react-window
С помощью react-window мы можем выводить списки с фиксированной высотой строки, а также списки с динамической высотой строки, пройдемся по каждому из них по очереди.
React-window со списком фиксированного размера
Если необходимо вывести элементы списка с фиксированной высотой строки, можно воспользоваться компонентом FixedSizeList
из react-window:
import { FixedSizeList } from "react-window";
const Row = ({ index, style }) => (
/* Adding style attribute is very important here
it supplies the row height to the elements. */
<div style={style}>Row {index}</div>
);
// Create a component with Fixed Size example
const ReactWindowFixedSizeEx = () => (
<FixedSizeList height={150} itemCount={100000} itemSize={35} width={300}>
{Row}
</FixedSizeList>
);
export { ReactWindowFixedSizeEx };
В приведенном выше примере мы установили высоту контейнера равной 150px, а высота каждой строки будет равна 35px.
Демонстрация: https://m5tkt9.csb.app/.
React-window со списком изменяемого размера
Если необходимо вывести элементы списка с динамической высотой строки, можно воспользоваться компонентом VariableSizeList
из react-window:
import { VariableSizeList } from "react-window";
import "./App.css";
const totalItemsCount = 100000;
// Set random row heights
const rowHeights = new Array(totalItemsCount)
.fill(true)
.map(() => 12 + Math.round(Math.random() * 50));
const getItemSize = (index) => rowHeights[index];
const Row = ({ index, style }) => (
<div style={style} className={index % 2 ? "odd" : "even"}>
Row {index}
</div>
);
// Create a component with Fixed Size example
const ReactWindowVariableSizeEx = () => (
<VariableSizeList
height={500}
itemCount={totalItemsCount}
itemSize={getItemSize}
width={300}
>
{Row}
</VariableSizeList>
);
export default ReactWindowVariableSizeEx;
В приведенном примере высота контейнера будет равна 500px, а высота ряда будет произвольной, и мы используем свойство itemSize
для указания высоты конкретного ряда.
В приведенном выше коде мы задаем произвольную высоту строки для каждой из строк, но в реальном приложении должна быть определенная логика для вычисления высот строк.
Демонстрация: https://64ynfm.csb.app/.
Отлично! Итак, мы рассмотрели различные способы реализации виртуальной прокрутки в react-приложении, вот ссылка на репозиторий GitHub с каждой из этих реализаций, если вы хотите проверить - https://github.com/imvedanshmehra/virtual-scroll-react.
Заключительные слова
Виртуализация - очень хорошая техника для повышения производительности приложения при рендеринге большого набора данных без пагинации, ее можно реализовать с нуля или использовать пакеты типа react-window
.
Вы можете сделать еще один шаг вперед и оптимизировать его, выполнив "ленивую" загрузку данных для отображения с помощью бесконечной прокрутки, которая является еще одной очень хорошей техникой для отображения большого набора данных.
Я надеюсь, что вы получили такое же удовольствие от чтения этой статьи, как и я от ее написания, и надеюсь, что вы сможете почерпнуть из нее что-то новое!