Каждый DOM-элемент требует ресурсов:
- Время рендеринга: React рекурсивно обходит компоненты, вычисляет разметку.
- Память: Хранение экземпляров компонентов, DOM-узлов, слушателей событий.
- Рефлоу/Репайнт: Изменения в списке запускают каскадные вычисления в браузере.
Попытка отрисовать 10 000 строк таблицы нередко приводит к блокировке UI потока на 10+ секунд. Вот простое решение, которое не работает:
// Плохая идея при 10k элементов:
const List = ({ items }) => (
<ul>
{items.map(item => <Item key={item.id} data={item} />)}
</ul>
);
Стратегия 1: Ленивая загрузка данных (Lazy Loading)
Не грузите все сразу. Разбейте данные на страницы или порции:
const [items, setItems] = React.useState([]);
const [page, setPage] = React.useState(1);
const fetchMore = () => {
fetch(`/api/data?page=${page}`)
.then(res => res.json())
.then(newItems => {
setItems(prev => [...prev, ...newItems]);
setPage(p => p + 1);
});
};
// Используем Intersection Observer для детекции скролла
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) fetchMore();
}, { threshold: 0.1 });
observer.observe(document.querySelector('#loader'));
return () => observer.disconnect();
}, []);
Минусы:
- Не решает проблему рендеринга при большом уже загруженном объёме данных.
- Невозможна моментальная навигация (прыжок к 5000-й строке).
Стратегия 2: Виртуализация рендеринга
Отрендерить только то, что видит пользователь. Основные шаги:
- Рассчитать \высоту контейнера\ списка и \позицию скролла.
- Вычислить индексы элементов, попадающих во viewport.
- Отображать только их, остальные заменять
padding
.
Реализация своими руками:
const VirtualList = ({ items, itemHeight, containerHeight }) => {
const containerRef = React.useRef();
const [startIdx, setStartIdx] = React.useState(0);
const visibleCount = Math.ceil(containerHeight / itemHeight);
const innerHeight = items.length * itemHeight;
const handleScroll = () => {
const top = containerRef.current.scrollTop;
const newStartIdx = Math.floor(top / itemHeight);
setStartIdx(newStartIdx);
};
const visibleItems = items.slice(startIdx, startIdx + visibleCount);
const offsetY = startIdx * itemHeight;
return (
<div ref={containerRef} style={{ height: containerHeight, overflowY: 'scroll' }} onScroll={handleScroll}>
<div style={{ height: innerHeight, position: 'relative' }}>
<div style={{ position: 'absolute', top: offsetY, width: '100%' }}>
{visibleItems.map(item => (
<Item key={item.id} height={itemHeight} data={item} />
))}
</div>
</div>
</div>
);
};
Критические оптимизации:
- Троттлинг событий прокрутки: Используйте
requestAnimationFrame
или Lodashthrottle
для снижения частоты пересчётов. - Фиксированная vs. динамическая высота элемента:
Если высота переменная, требуются сложные расчёты либо API наподобиеResizeObserver
.
Библиотеки для сложных сценариев
Для динамических высот или горизонтальных списков:
- react-window: Минималистичная, эффективная.
- react-virtualized: Продвинутые фичи (авторазмеры, таблицы).
- @tanstack/react-virtual (ex. react-virtual): Композиционный API, поддержка React 18.
Пример с реакт-виндоу:
import { FixedSizeList } from 'react-window';
const Row = ({ index, style }) => <div style={style}>Item {index}</div>;
const App = () => (
<FixedSizeList
height={600} // Высота контейнера
itemCount={10000}
itemSize={50} // Фиксированная высота элемента
width="100%"
>
{Row}
</FixedSizeList>
);
Edge Cases и нюансы
- Быстрота vs. точность: В виртуализации размер viewport часто искусственно увеличивают на 5-10% (overscan), чтобы избежать "мелькания" при скролле.
- Асинхронные данные: Если элементы запрашиваются с сервера, требуется предзагрузка соседних элементов.
- Мобильные устройства: Не забывайте про инерционный скролл — пассивные слушатели (
{ passive: true }
). - React 18 и Concurrent Mode: Используйте
startTransition
для приоритизации рендеринга видимых элементов над фоновыми.
Когда виртуализация не нужна?
content-visibility: auto;
(CSS): Современные браузеры откладывают рендеринг элементов за пределами viewport. Отлично работает для статичных списков.- Иерархическая разбивка: Если список имеет N уровней вложенности (например, дерево файлов), можно применять "ленивое" раскрытие узлов.
Вывод
Оптимизация списков — компромисс между сложностью реализации и требованиями UX. Начните с ленивой загрузки. Если загружено >500 элементов — внедряйте виртуализацию. Используйте готовые решения, но понимайте их границы: для списков с динамической высотой элементов react-virtualized даст бóльшую гибкость, чем react-window. Финальный замер — профилирование в React DevTools и Chrome Performance. Скроллинг свыше 60 FPS достижим для 100k записей при правильном подходе.