Виртуализация длинных списков
Виртуализация (windowing) — техника рендеринга только видимых элементов длинного списка: в DOM находится ~20-30 строк вместо тысяч, что радикально сокращает потребление памяти и время рендеринга.
Зачем нужно
Рендеринг 10 000 DOM-элементов создаёт серьёзную нагрузку на память и замедляет скролл. Виртуализация держит в DOM только ~30 видимых элементов + буфер: скролл плавный, память под контролем, React reconciliation быстрый.
Где используется
- Таблицы с тысячами строк (логи, транзакции, отчёты)
- Бесконечные ленты (соцсети, чат-история)
- Выпадающие списки с тысячами опций (комбобоксы)
- Файловые менеджеры и tree view
Основной контент
Концепция windowing
Реальный DOM: Виртуализированный DOM:
┌─────────────┐ ┌─────────────┐
│ Item 1 │ │ spacer (top)│ ← высота отсутствующих эл.
│ Item 2 │ │ Item 6 │
│ Item 3 │ │ Item 7 │ ← только видимые
│ ... │ │ Item 8 │
│ Item 10000 │ │ Item 9 │
└─────────────┘ │ spacer(bot) │
10000 DOM nodes! └─────────────┘
~10 DOM nodes!
react-virtual (TanStack Virtual)
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
function VirtualList({ items }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: => parentRef.current,
estimateSize: => 60, // Предполагаемая высота строки (px)
overscan: 5, // Дополнительные элементы вне viewport
});
return (
<div
ref={parentRef}
style={{ height: '600px', overflow: 'auto' }}
>
{/* Контейнер с полной высотой для правильного scrollbar */}
<div style={{ height: virtualizer.getTotalSize + 'px', position: 'relative' }}>
{virtualizer.getVirtualItems.map(virtualItem => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: virtualItem.start() + 'px',
width: '100%',
height: virtualItem.size + 'px',
}}
>
<ListItem item={items[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}
react-window (альтернатива для фиксированных размеров)
import { FixedSizeList } from 'react-window';
const Row = ({ index, style, data }) => (
<div style={style} className="list-row">
{data[index].name}
</div>
);
function SimpleList({ items }) {
return (
<FixedSizeList
height={600}
width="100%"
itemCount={items.length}
itemSize={60} // Высота строки в px
itemData={items}
>
{Row}
</FixedSizeList>
);
}
Виртуализация с переменной высотой
import { VariableSizeList } from 'react-window';
import { useCallback, useRef } from 'react';
function VariableList({ items }) {
const listRef = useRef(null);
const rowHeights = useRef({});
const getItemSize = useCallback(
index => rowHeights.current[index] ?? 80,
);
const setRowHeight = useCallback((index, size) => {
rowHeights.current = { ...rowHeights.current, [index]: size };
listRef.current?.resetAfterIndex(index);
}, );
return (
<VariableSizeList
ref={listRef}
height={600}
itemCount={items.length}
itemSize={getItemSize}
estimatedItemSize={80}
>
{({ index, style }) => (
<MeasuredRow
style={style}
item={items[index]}
onHeightChange={h => setRowHeight(index, h)}
/>
)}
</VariableSizeList>
);
}
Частые ошибки
- Виртуализация при < 100 элементах — overhead не оправдан,
content-visibility: autoдостаточно - Отсутствие
keyна виртуальных элементах — React неправильно сопоставляет компоненты - Динамическая высота без
resetAfterIndex— некорректное позиционирование - Вложенный скролл внутри виртуализированного списка — конфликт scroll handlers
Связанные темы
- _MOC Производительность
- React.memo и useMemo
- content-visibility -- lazy rendering
- React -- виртуальный DOM и reconciliation