Виртуализация длинных списков: цифры до и после
Список из 10 000 строк вешает вкладку на 1,5 секунды при монтировании и лагает при скролле. Разбираем, что именно тормозит, как виртуализация меняет цифры и где она ломается.

С чего начинается лаг
Таблица заказов на 10 000 строк. React рендерит все узлы сразу: в DOM попадает под сотню тысяч элементов, если в каждой строке 5–8 ячеек с иконками и кнопками. На ноутбуке с Chrome вкладка замирает на монтировании, скролл идёт рывками, а открытие фильтра, который меняет один флаг в состоянии, перезапускает согласование по всему дереву.
Проблема не в React и не в объёме данных как таковом. Проблема в том, что браузер держит в памяти и пересчитывает layout для узлов, которых пользователь не видит. Виртуализация убирает из DOM всё, что за пределами вьюпорта, и рендерит только видимое окно плюс небольшой буфер. Ниже — что именно меняется в цифрах, как это измерить у себя и где приём даёт трещину.
Прогрев и предзагрузка данных — отдельная тема, здесь только рендеринг уже полученного массива.
Что мерить и на чём
Без замеров разговор про оптимизацию бессмысленный. Минимальный набор инструментов, все встроены в Chrome DevTools (тестировалось в Chrome 130):
- Performance panel — запись профиля при монтировании и при скролле. Смотрим на суммарное время Scripting, Rendering (пересчёт layout) и Painting.
- Счётчик узлов DOM — вкладка Performance monitor показывает текущее число DOM nodes в реальном времени.
- React Profiler — вкладка Profiler в React DevTools: сколько компонентов перерендерилось и сколько это заняло.
Условия замеров ниже: список из 10 000 объектов, строка — компонент с датой, статусом-бейджем и двумя кнопками. Chrome 130, MacBook на Apple M1, throttling выключен. Ваши абсолютные цифры будут другими — важна разница до и после и порядок величин.
Цифры «до»: наивный рендер
Массив прогоняется через map, каждый элемент — отдельный компонент строки. Никакой виртуализации.
- DOM nodes: ~85 000.
- Время первого монтирования (Scripting + Rendering по Performance): около 1400–1700 мс, вкладка не отвечает.
- Пиковое потребление памяти вкладкой заметно растёт из-за размера дерева.
- Скролл: дропы кадров, FPS проваливается ниже 30 на быстром прокруте.
- Смена одного фильтра в состоянии родителя: React переобходит все 10 000 строк, даже если 9 990 из них не изменились.
Первое, что напрашивается, — обмазать строки React.memo. Это снимает лишние перерендеры при смене состояния родителя, но не решает главную боль: 85 000 узлов всё равно живут в DOM и грузят layout при каждом изменении геометрии. Мемоизация лечит Scripting, но не Rendering.
Как работает виртуализация
Идея: контейнер получает фиксированную высоту и скроллится. Внутри — распорка (spacer) высотой во весь список, чтобы полоса прокрутки была честной. Реально в DOM находятся только строки, попадающие в видимое окно, плюс буфер сверху и снизу (overscan). При скролле по позиции scrollTop пересчитывается диапазон видимых индексов, и рендерятся новые строки, а ушедшие за край размонтируются.
Готовые библиотеки
Писать это руками не нужно. Для React в 2024–2025 фактический стандарт — TanStack Virtual (пакет @tanstack/react-virtual, ветка v3). Он headless: даёт математику и позиции, а разметку и стили пишете сами. Второй распространённый вариант — react-window от Брайана Вона, чуть более «батарейка в комплекте», но менее гибкий.
Проверьте актуальные имена хуков и опций в документации версии, которую ставите: API между мажорными версиями TanStack менялся. Ниже — типовой каркас для @tanstack/react-virtual v3.
import { useRef } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
// orders — массив из 10 000 объектов
function OrdersList({ orders }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: orders.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48, // ожидаемая высота строки в px
overscan: 8, // сколько строк рендерить за пределами окна
});
return (
<div
ref={parentRef}
style={{ height: 600, overflow: "auto" }}
>
{/* распорка на полную высоту списка */}
<div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
{virtualizer.getVirtualItems().map((virtualRow) => {
const order = orders[virtualRow.index];
return (
<div
key={order.id}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: virtualRow.size,
transform: `translateY(${virtualRow.start}px)`,
}}
>
<OrderRow order={order} />
</div>
);
})}
</div>
</div>
);
}Ключевая деталь — строки позиционируются через transform: translateY, а не через изменение высоты распорок сверху. Трансформ не запускает reflow соседей и обрабатывается на композитном слое, поэтому скролл остаётся плавным.
Цифры «после»
Тот же список, те же условия, тот же компонент строки, обёрнутый в виртуализацию с overscan 8:
| Показатель | Наивный рендер | С виртуализацией |
|---|---|---|
| DOM nodes | ~85 000 | ~200–300 |
| Монтирование (Scripting+Rendering) | 1400–1700 мс | 40–80 мс |
| FPS при быстром скролле | <30, дропы | ~55–60 |
| Перерендер строк при смене фильтра | 10 000 | только видимые (~15–25) |
Порядок разницы на монтировании — десятки раз, потому что браузер строит layout для двухсот узлов вместо восьмидесяти тысяч. Это не микрооптимизация, а смена режима работы.
Виртуализация экономит не на алгоритме, а на физике: браузер не платит за то, чего нет в DOM. Мемоизация делает быстрее работу React, виртуализация вообще отменяет часть этой работы.
Где подход ломается
Виртуализация — не бесплатная кнопка «сделать быстро». Граничные случаи, о которых узнают в проде:
- Строки разной высоты. Если высота заранее неизвестна (переносы текста, раскрывающиеся блоки),
estimateSizeошибается, и полоса прокрутки «прыгает» при измерении реальных размеров. TanStack Virtual умеет динамическое измерение черезmeasureElement, но это дороже и может дёргать скролл. Проверяйте на реальном контенте, а не на строках одинаковой длины. - Ctrl+F по странице. Браузерный поиск не найдёт текст в строках, которых нет в DOM. Для таблиц с важным поиском нужен собственный поиск по данным, а не по разметке.
- Доступность и якорные ссылки. Скринридеры и переходы по
#anchorломаются на несуществующих узлах. Прыжок к элементу делается черезscrollToIndex, а не через нативный якорь. - Горизонтальный скролл и залипающие колонки. Абсолютное позиционирование строк усложняет sticky-заголовки и фиксированные колонки таблицы. Часто приходится выносить заголовок за пределы скролл-контейнера.
- SEO и печать. Виртуализированный список отдаёт краулеру и на печать только видимое окно. Для контента, который должен индексироваться, виртуализация противопоказана.
Когда виртуализация не нужна
Если в списке стабильно меньше нескольких сотен элементов, накладные расходы на измерение и абсолютное позиционирование могут оказаться дороже выигрыша, а перечисленные проблемы с поиском и доступностью вы получите даром. Сначала замерьте наивный рендер. Если монтирование укладывается в бюджет кадра и скролл плавный — оставьте как есть. Порог, за которым виртуализация начинает окупаться, обычно лежит в районе нескольких сотен–тысячи строк, но зависит от тяжести компонента строки.
Что делать дальше
Порядок действий, а не абстрактный «оптимизируйте»:
- Снимите профиль наивного рендера в Performance и запишите три цифры: DOM nodes, время монтирования, FPS при скролле. Без базовой линии вы не докажете, что стало лучше.
- Если строки одинаковой высоты — начинайте с фиксированного
estimateSize. Динамическое измерение подключайте, только когда фиксированная высота реально не подходит. - После внедрения снимите профиль повторно на тех же данных и сравните. Ищите не «стало быстрее на глаз», а разницу в миллисекундах и узлах.
- Проверьте граничные случаи из раздела выше именно на вашем контенте: поиск, доступность, sticky-заголовок.
Следующая тема для проработки — виртуализация с бесконечной подгрузкой (infinite scroll) и восстановление позиции скролла при возврате на страницу. Это отдельная механика поверх той же математики окна.
Субтитры и таймкоды: Расшифровка речи и субтитры по правилам читаемости: 42 знака в строке, две строки, минимум секунда на реплику.
Спросить за 40 ₽Источники: TanStack Virtual — официальная документация, Chrome DevTools: Performance features reference
Частые вопросы
У меня react-window, а не TanStack Virtual — код из статьи подойдёт?
А если строки разной высоты и заранее неизвестны?
Скролл всё равно лагает после внедрения — что проверить?
Ломается ли поиск по странице через Ctrl+F?
Хватит ли просто обернуть строки в React.memo вместо виртуализации?
Виртуализация мешает индексации поисковиками?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.