Современные интерфейсы нередко работают с большими массивами данных: списками пользователей, логами, таблицами товаров, сообщениями чатов, результатами поиска. Когда количество элементов достигает сотен или тысяч, возникает фундаментальная проблема производительности.
При традиционном подходе каждый элемент списка отображается как отдельный DOM-узел. Браузер должен:
Если список содержит 1000–10000 элементов, это приводит к нескольким негативным эффектам:
Основные проблемы:
Даже при использовании виртуального DOM (React) реальный DOM остаётся ограничивающим фактором.
Решением становится виртуализация списков.
Виртуализация — это техника отображения только той части списка, которая видна пользователю в текущий момент времени.
Вместо рендера всех элементов:
10000 элементов
рендерится только:
видимая область + небольшой буфер
Например:
экран показывает ~12 элементов
+ буфер 5 сверху
+ буфер 5 снизу
=> всего ~22 DOM элемента
Таким образом:
10000 элементов данных
22 элемента в DOM
Данные остаются полными, но интерфейс работает только с небольшим подмножеством.
Виртуализация базируется на трёх ключевых механизмах:
Контейнер прокрутки имеет известную высоту.
height: 400px
overflow-y: auto
По текущему значению scrollTop вычисляется:
начальный индекс
конечный индекс
Формула:
visibleStartIndex = scrollTop / itemHeight
visibleEndIndex = visibleStartIndex + visibleItemCount
Чтобы сохранить корректную прокрутку, создаётся виртуальная высота списка.
totalHeight = itemCount * itemHeight
Но реальные элементы позиционируются абсолютно внутри контейнера.
Библиотека Radix UI предоставляет низкоуровневые, доступные (accessible) компоненты интерфейса. Многие из них работают со списками:
Когда такие компоненты содержат сотни или тысячи элементов, без виртуализации возникают серьёзные проблемы:
Поэтому виртуализация становится важной архитектурной техникой при интеграции Radix UI.
В контексте виртуализации чаще всего используются:
Контролируемая область прокрутки.
@radix-ui/react-scroll-area
Предоставляет:
Компонент выбора значения из списка.
@radix-ui/react-select
Проблема:
1000+ option элементов
Командная палитра.
cmd + k
Обычно содержит:
команды
действия
результаты поиска
Количество элементов может достигать тысяч.
Структура виртуализированного списка:
ScrollContainer
VirtualSpacer (общая высота)
AbsoluteItems (видимые элементы)
Схематично:
|--------------------|
| Scroll container |
| |
| [ item 120 ] |
| [ item 121 ] |
| [ item 122 ] |
| [ item 123 ] |
| |
|--------------------|
Фактически:
10000 элементов
рендерится только 20
items.length = 10000
itemHeight = 40px
containerHeight = 400px
Количество видимых элементов:
visibleCount = containerHeight / itemHeight
import { useState } fr om "react"
function VirtualList({ items }) {
const itemHeight = 40
const containerHeight = 400
const [scrollTop, setScrollTop] = useState(0)
const startIndex = Math.floor(scrollTop / itemHeight)
const visibleCount = Math.ceil(containerHeight / itemHeight)
const endIndex = startIndex + visibleCount + 5
const visibleItems = items.slice(startIndex, endIndex)
const totalHeight = items.length * itemHeight
return (
<div
style={{
height: containerHeight,
overflowY: "auto"
}}
onScr oll={(e) => setScrollTop(e.currentTarget.scrollTop)}
>
<div style={{ height: totalHeight, position: "relative" }}>
{visibleItems.map((item, i) => {
const index = startIndex + i
return (
<div
key={index}
style={{
position: "absolute",
top: index * itemHeight,
height: itemHeight,
left: 0,
right: 0
}}
>
{item}
</div>
)
})}
</div>
</div>
)
}
В DOM присутствует лишь небольшое число элементов.
Если рендерить только строго видимые элементы, при быстром скролле возможны артефакты:
Поэтому используется overscan.
Это дополнительная область элементов:
overscan = 5
Реальный диапазон:
startIndex - overscan
endIndex + overscan
Это обеспечивает:
Radix ScrollArea часто используется как контейнер прокрутки.
import * as ScrollArea from "@radix-ui/react-scroll-area"
Виртуализация может располагаться внутри Viewport.
<ScrollArea.Root>
<ScrollArea.Viewport
onScr oll={handleScroll}
style={{ height: 400 }}
>
{virtualizedItems}
</ScrollArea.Viewport>
<ScrollArea.Scrollbar orientation="vertical" />
</ScrollArea.Root>
Viewport работает как scroll-container.
В реальных проектах чаще применяется готовая библиотека виртуализации.
Одна из наиболее популярных:
@tanstack/react-virtual
Преимущества:
npm install @tanstack/react-virtual
import { useVirtualizer } from "@tanstack/react-virtual"
import { useRef } from "react"
function VirtualList({ items }) {
const parentRef = useRef()
const rowVirtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 40,
overscan: 5
})
return (
<div
ref={parentRef}
style={{
height: 400,
overflow: "auto"
}}
>
<div
style={{
height: rowVirtualizer.getTotalSize(),
position: "relative"
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.key}
style={{
position: "absolute",
top: 0,
transform: `translateY(${virtualRow.start}px)`,
height: virtualRow.size
}}
>
{items[virtualRow.index]}
</div>
))}
</div>
</div>
)
}
Проблема:
<Select>
5000 <Select.Item>
</Select>
Открытие меню вызывает:
Виртуализация решает проблему.
Select.Content
ScrollArea
VirtualizedList
Select.Item
<Select.Content>
<ScrollArea.Root>
<ScrollArea.Viewport>
<VirtualList items={items} />
</ScrollArea.Viewport>
</ScrollArea.Root>
</Select.Content>
Внутри виртуализированного списка размещаются
Select.Item.
Несколько особенностей требуют внимания.
Radix активно использует roving focus.
Это означает:
фокус перемещается между элементами
Если элемент не существует в DOM (виртуализация), возникает проблема.
Решение:
При навигации клавишами:
ArrowDown
ArrowUp
необходимо прокручивать список автоматически.
Виртуализатор должен поддерживать:
scrollToIndex(index)
Компоненты Radix часто поддерживают typeahead.
Например:
набор текста -> поиск элемента
После поиска нужно:
scrollToIndex(foundIndex)
Иногда элементы списка имеют разную высоту.
Например:
сообщения чата
карточки
комментарии
В этом случае фиксированная высота невозможна.
Решение:
Библиотеки виртуализации используют:
ResizeObserver
или ручное измерение.
Эти термины часто используются как синонимы, но имеют различия.
Отображение окна элементов.
startIndex -> endIndex
Более общий термин.
Включает:
Виртуализировать можно не только вертикальные списки.
Примеры:
Формулы аналогичны:
scrollLeft
itemWidth
visibleCount
Большие таблицы могут иметь:
1000 строк
1000 колонок
DOM:
1 000 000 ячеек
Виртуализация применяется одновременно:
по вертикали
по горизонтали
Так работают:
Сравнение:
| Количество элементов | DOM без виртуализации | DOM с виртуализацией |
|---|---|---|
| 1000 | 1000 | ~20 |
| 5000 | 5000 | ~25 |
| 10000 | 10000 | ~30 |
Разница:
Элементы списка часто оборачиваются в:
React.memo
Это предотвращает лишние рендеры.
Ключи должны быть стабильными:
key={item.id}
а не:
key={index}
Элементы списка должны быть лёгкими.
Нежелательно внутри:
Некоторые разработчики делают:
display: none
Это не виртуализация.
DOM всё равно содержит элементы.
Слишком маленький overscan вызывает:
мерцание
Слишком большой:
лишний DOM
Оптимальный диапазон:
5–10 элементов
Если элементы не позиционируются абсолютно, браузер выполняет полный layout списка.
Не всегда есть смысл усложнять архитектуру.
Если список содержит:
до 100 элементов
виртуализация обычно не требуется.
Она оправдана при:
500+
1000+
10000+
поиск по 10000 пользователей
сотни команд
десятки тысяч строк
большие datasets
чат-интерфейсы
Типичная структура:
Radix Component
ScrollArea
Virtualizer
Visible Items
Система обеспечивает:
Виртуализация становится ключевой техникой построения интерфейсов, работающих с большими объёмами данных, и гармонично интегрируется с архитектурой низкоуровневых компонентов Radix UI.