Виртуализация списков

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

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


Компонент ScrollArea и контейнер виртуализации

Radix UI предоставляет компонент ScrollArea, который заменяет стандартный div с overflow: auto и обеспечивает дополнительные возможности управления скроллом. В комбинации с виртуализацией он становится базовым контейнером для больших списков.

Пример базовой структуры:

import * as ScrollArea from '@radix-ui/react-scroll-area';

<ScrollArea.Root style={{ height: 400, width: 300 }}>
  <ScrollArea.Viewport>
    { /* Здесь будут виртуализированные элементы */ }
  </ScrollArea.Viewport>
  <ScrollArea.Scrollbar orientation="vertical">
    <ScrollArea.Thumb />
  </ScrollArea.Scrollbar>
</ScrollArea.Root>

Важные моменты:

  • Viewport определяет область видимости контента.
  • Scrollbar и Thumb обеспечивают стилизованный скролл, полностью совместимый с виртуализацией.
  • Высота Root должна быть фиксированной для корректного расчёта видимых элементов.

Создание виртуализированного списка

Для виртуализации удобно использовать сторонние библиотеки, например, react-virtual, но Radix UI предоставляет гибкую архитектуру, позволяющую интегрировать её напрямую.

Пример использования виртуализации с помощью react-virtual:

import { useVirtual } from '@tanstack/react-virtual';
import * as ScrollArea from '@radix-ui/react-scroll-area';

function VirtualList({ items }) {
  const parentRef = React.useRef();
  const rowVirtualizer = useVirtual({
    size: items.length,
    parentRef,
    estimateSize: () => 35,
  });

  return (
    <ScrollArea.Root style={{ height: 400, width: 300 }}>
      <ScrollArea.Viewport ref={parentRef}>
        <div style={{ height: rowVirtualizer.totalSize, position: 'relative' }}>
          {rowVirtualizer.virtualItems.map(virtualRow => (
            <div
              key={virtualRow.index}
              style={{
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                height: `${virtualRow.size}px`,
                transform: `translateY(${virtualRow.start}px)`
              }}
            >
              {items[virtualRow.index]}
            </div>
          ))}
        </div>
      </ScrollArea.Viewport>
      <ScrollArea.Scrollbar orientation="vertical">
        <ScrollArea.Thumb />
      </ScrollArea.Scrollbar>
    </ScrollArea.Root>
  );
}

Ключевые аспекты:

  • parentRef используется для отслеживания видимой области.
  • rowVirtualizer.totalSize задаёт полную высоту списка, что необходимо для корректного скролла.
  • Каждый элемент позиционируется абсолютно через transform: translateY, чтобы избежать рендеринга невидимых элементов.

Оптимизация производительности

При работе с большими списками важно учитывать следующие моменты:

  1. Фиксированная высота элементов — упрощает расчёт виртуальных позиций.
  2. Memoization элементов — предотвращает повторные рендеры.
  3. Debounce скролла — снижает частоту обновления виртуальных элементов.
  4. Lazy-load данных — для динамических списков подгружать элементы партиями.

Взаимодействие с другими компонентами Radix UI

Виртуализированный список можно сочетать с другими компонентами:

  • Checkbox и RadioGroup — для списков с выбором элементов.
  • Tooltip и Popover — элементы внутри виртуализированного списка могут иметь всплывающие подсказки без потери производительности.
  • Collapsible — позволяет внедрять сворачиваемые группы элементов внутри виртуализированного списка.

При этом важно учитывать, что события и состояние компонентов должны быть привязаны к индексам виртуальных элементов, а не к их уникальным ключам DOM, чтобы избежать проблем при повторном рендере.


Обработка динамического размера элементов

Если элементы списка имеют переменную высоту, Radix UI не предоставляет встроенного механизма. В таких случаях используют сторонние решения, например:

  • react-virtual с measureRef — измеряет реальную высоту каждого элемента.
  • Placeholder элементы — задаются минимальной высотой для корректного скролла до измерения.
const rowVirtualizer = useVirtual({
  size: items.length,
  parentRef,
  estimateSize: (index) => estimatedHeights[index] || 35,
  measureRef: (el, index) => {
    if(el) measuredHeights[index] = el.getBoundingClientRect().height;
  }
});

Поддержка клавиатурной навигации

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

  • ArrowUp / ArrowDown для перемещения фокуса.
  • Home / End для перехода к началу или концу списка.
  • Tab для перехода между интерактивными элементами внутри списка.

Важно синхронизировать виртуальные позиции с фокусом, чтобы элемент, получивший фокус, всегда был в видимой области:

rowVirtualizer.scrollToIndex(focusedIndex, { align: 'auto' });

Использование с анимациями и переходами

При необходимости плавного появления элементов можно использовать CSS transitions или библиотеки вроде framer-motion. Основная сложность — синхронизация анимации с абсолютным позиционированием виртуализированных элементов.

  • Анимации opacity и transform работают лучше всего.
  • Высота элементов обычно не анимируется, чтобы не нарушать расчёт виртуальной области.

Эффективная виртуализация в Radix UI позволяет создавать масштабируемые и доступные интерфейсы, обеспечивая высокую производительность и совместимость с комплексными компонентами экосистемы.