Виртуализация длинных списков

Современные интерфейсы нередко работают с большими массивами данных: списками пользователей, логами, таблицами товаров, сообщениями чатов, результатами поиска. Когда количество элементов достигает сотен или тысяч, возникает фундаментальная проблема производительности.

При традиционном подходе каждый элемент списка отображается как отдельный DOM-узел. Браузер должен:

  • создать все элементы в DOM
  • рассчитать стили
  • выполнить layout
  • выполнить отрисовку
  • обрабатывать события для каждого элемента

Если список содержит 1000–10000 элементов, это приводит к нескольким негативным эффектам:

Основные проблемы:

  • долгий initial render
  • медленный скроллинг
  • высокий расход памяти
  • частые перерасчёты layout
  • снижение FPS интерфейса

Даже при использовании виртуального DOM (React) реальный DOM остаётся ограничивающим фактором.

Решением становится виртуализация списков.


Концепция виртуализации

Виртуализация — это техника отображения только той части списка, которая видна пользователю в текущий момент времени.

Вместо рендера всех элементов:

10000 элементов

рендерится только:

видимая область + небольшой буфер

Например:

экран показывает ~12 элементов
+ буфер 5 сверху
+ буфер 5 снизу

=> всего ~22 DOM элемента

Таким образом:

10000 элементов данных
22 элемента в DOM

Данные остаются полными, но интерфейс работает только с небольшим подмножеством.


Основная идея работы

Виртуализация базируется на трёх ключевых механизмах:

1. Контейнер фиксированной высоты

Контейнер прокрутки имеет известную высоту.

height: 400px
overflow-y: auto

2. Вычисление видимого диапазона

По текущему значению scrollTop вычисляется:

начальный индекс
конечный индекс

Формула:

visibleStartIndex = scrollTop / itemHeight
visibleEndIndex = visibleStartIndex + visibleItemCount

3. Имитация полной высоты списка

Чтобы сохранить корректную прокрутку, создаётся виртуальная высота списка.

totalHeight = itemCount * itemHeight

Но реальные элементы позиционируются абсолютно внутри контейнера.


Почему виртуализация актуальна при использовании Radix UI

Библиотека Radix UI предоставляет низкоуровневые, доступные (accessible) компоненты интерфейса. Многие из них работают со списками:

  • Select
  • Dropdown Menu
  • Combobox
  • Command Palette
  • ScrollArea
  • Popover списков
  • Autocomplete

Когда такие компоненты содержат сотни или тысячи элементов, без виртуализации возникают серьёзные проблемы:

  • медленная работа меню
  • задержки открытия
  • лаги при прокрутке

Поэтому виртуализация становится важной архитектурной техникой при интеграции Radix UI.


Компоненты Radix UI, связанные со списками

В контексте виртуализации чаще всего используются:

ScrollArea

Контролируемая область прокрутки.

@radix-ui/react-scroll-area

Предоставляет:

  • кастомный скроллбар
  • контроль overflow
  • доступность
  • стабильный layout

Select

Компонент выбора значения из списка.

@radix-ui/react-select

Проблема:

1000+ option элементов

Command

Командная палитра.

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.

Это дополнительная область элементов:

overscan = 5

Реальный диапазон:

startIndex - overscan
endIndex + overscan

Это обеспечивает:

  • плавную прокрутку
  • отсутствие “мигания” элементов

Интеграция виртуализации с Radix ScrollArea

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.


Использование библиотеки react-virtual

В реальных проектах чаще применяется готовая библиотека виртуализации.

Одна из наиболее популярных:

@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>
  )
}

Виртуализация внутри Radix Select

Проблема:

<Select>
  5000 <Select.Item>
</Select>

Открытие меню вызывает:

  • создание тысяч DOM элементов
  • долгую отрисовку

Виртуализация решает проблему.


Архитектура

Select.Content
    ScrollArea
        VirtualizedList
            Select.Item

Пример структуры

<Select.Content>
  <ScrollArea.Root>
    <ScrollArea.Viewport>
      <VirtualList items={items} />
    </ScrollArea.Viewport>
  </ScrollArea.Root>
</Select.Content>

Внутри виртуализированного списка размещаются Select.Item.


Особенности виртуализации с Radix

Несколько особенностей требуют внимания.

1. Управление фокусом

Radix активно использует roving focus.

Это означает:

фокус перемещается между элементами

Если элемент не существует в DOM (виртуализация), возникает проблема.

Решение:

  • всегда держать буфер элементов
  • синхронизировать индекс фокуса

2. Keyboard navigation

При навигации клавишами:

ArrowDown
ArrowUp

необходимо прокручивать список автоматически.

Виртуализатор должен поддерживать:

scrollToIndex(index)

3. Поиск внутри списка

Компоненты Radix часто поддерживают typeahead.

Например:

набор текста -> поиск элемента

После поиска нужно:

scrollToIndex(foundIndex)

Динамическая высота элементов

Иногда элементы списка имеют разную высоту.

Например:

сообщения чата
карточки
комментарии

В этом случае фиксированная высота невозможна.

Решение:

  • измерение DOM элементов
  • кеширование размеров

Библиотеки виртуализации используют:

ResizeObserver

или ручное измерение.


Windowing vs Virtualization

Эти термины часто используются как синонимы, но имеют различия.

Windowing

Отображение окна элементов.

startIndex -> endIndex

Virtualization

Более общий термин.

Включает:

  • windowing
  • виртуальную высоту
  • оптимизацию DOM

Горизонтальная виртуализация

Виртуализировать можно не только вертикальные списки.

Примеры:

  • галереи
  • таблицы
  • карусели

Формулы аналогичны:

scrollLeft
itemWidth
visibleCount

Двумерная виртуализация (таблицы)

Большие таблицы могут иметь:

1000 строк
1000 колонок

DOM:

1 000 000 ячеек

Виртуализация применяется одновременно:

по вертикали
по горизонтали

Так работают:

  • большие таблицы данных
  • spreadsheets

Производительность

Сравнение:

Количество элементов DOM без виртуализации DOM с виртуализацией
1000 1000 ~20
5000 5000 ~25
10000 10000 ~30

Разница:

  • десятки раз меньше DOM
  • значительно быстрее скролл
  • меньше memory pressure

Дополнительные оптимизации

Memoization

Элементы списка часто оборачиваются в:

React.memo

Это предотвращает лишние рендеры.


Stable keys

Ключи должны быть стабильными:

key={item.id}

а не:

key={index}

Avoid expensive components

Элементы списка должны быть лёгкими.

Нежелательно внутри:

  • сложные графики
  • heavy animations
  • сложные layout вычисления

Типичные ошибки

Рендер всех элементов скрытыми

Некоторые разработчики делают:

display: none

Это не виртуализация.

DOM всё равно содержит элементы.


Неправильный overscan

Слишком маленький overscan вызывает:

мерцание

Слишком большой:

лишний DOM

Оптимальный диапазон:

5–10 элементов

Отсутствие абсолютного позиционирования

Если элементы не позиционируются абсолютно, браузер выполняет полный layout списка.


Когда виртуализация не нужна

Не всегда есть смысл усложнять архитектуру.

Если список содержит:

до 100 элементов

виртуализация обычно не требуется.

Она оправдана при:

500+
1000+
10000+

Практические сценарии использования

Autocomplete

поиск по 10000 пользователей

Command palette

сотни команд

Логи

десятки тысяч строк

Таблицы данных

большие datasets

История сообщений

чат-интерфейсы

Итоговая архитектура виртуализированного списка в Radix UI

Типичная структура:

Radix Component
    ScrollArea
        Virtualizer
            Visible Items

Система обеспечивает:

  • высокую производительность
  • стабильный скроллинг
  • минимальный DOM
  • масштабируемость интерфейса

Виртуализация становится ключевой техникой построения интерфейсов, работающих с большими объёмами данных, и гармонично интегрируется с архитектурой низкоуровневых компонентов Radix UI.