Skip to content

Справочник

Типы TypeScript

Все публичные типы экспортируются из корня пакета:

ts
import type {
  // Определение столбца для VirtualTable
  ColumnDef,

  // Определение группы для GroupedVirtualList
  GroupDef,

  // Выравнивание прокрутки
  ScrollAlign, // 'start' | 'center' | 'end' | 'auto'

  // Опции для scrollTo/scrollToOffset — { behavior?: 'auto' | 'smooth' }
  ScrollBehaviorOptions,

  // Видимый диапазон, возвращаемый useVirtualScroll
  VisibleRange, // { start: number; end: number }

  // Публичный API VirtualList (используйте вместо InstanceType для generic-компонентов)
  VirtualListExpose,

  // Публичный API GroupedVirtualList
  GroupedVirtualListExpose,

  // Payload события сортировки от VirtualTable
  SortChange, // { key: string; direction: 'asc' | 'desc' | null }

  // Низкоуровневый тип для строки GroupedVirtualList
  VirtualRow,
  VirtualRowType,

  // Типы дерева, экспортируемые из VirtualTree
  TreeNode,
  FlatTreeRow,

  // Провайдер размера для PositionManager / useVirtualScroll
  SizeProvider, // number | ((index: number) => number)

  // Типы опций/возврата для useRowSelection
  UseRowSelectionOptions,
  UseRowSelectionReturn,

  // Типы опций/возврата для useVisibilityTracker
  UseVisibilityTrackerOptions,
  UseVisibilityTrackerReturn,
} from 'vue-virtual-scroller-kit'

VirtualListExpose против InstanceType

Generic Vue SFC (generic="T") несовместимы с InstanceType<typeof Component>. Вместо этого используйте выделенные интерфейсы публичного API:

ts
// ✗ Не работает для generic SFC
const listRef = ref<InstanceType<typeof VirtualList> | null>(null)

// ✓ Правильно
import type { VirtualListExpose } from 'vue-virtual-scroller-kit'
const listRef = ref<VirtualListExpose | null>(null)

Доступность

ВозможностьРеализация
role="list" / role="listitem"Применяется к контейнеру VirtualList и каждой видимой строке
aria-rowcountУстанавливается в общее число элементов на контейнере списка
aria-rowindexУстанавливается в index + 1 на каждой видимой строке
aria-busyУстанавливается в "true" на контейнере, пока isLoading истинно
role="grid" / role="gridcell"Используется в VirtualGrid
aria-rowindex / aria-colindexУстанавливается на ячейках VirtualGrid
role="treeitem"Используется на строках VirtualTree
aria-expanded / aria-levelУстанавливается на строках дерева с дочерними элементами
role="combobox" / role="listbox" / role="option"Используется в VirtualSelect
aria-expanded / aria-haspopup / aria-selectedУстанавливается на триггере select и опциях
Поддержка клавиатурыПолная навигация с клавиатуры через useVirtualKeyboardNav

Поддержка SSR

Все компоненты рендерят первые ssrPreloadCount строк (по умолчанию 20) на сервере с оценочными высотами. Клиентская гидратация постепенно заменяет оценочные позиции измеренными высотами через ResizeObserver — без сдвига разметки и прыжков прокрутки.

Компоненты, использующие API, доступные только в браузере (ResizeObserver, IntersectionObserver, requestAnimationFrame, window.scroll), защищают эти вызовы проверкой typeof window !== 'undefined' и onMounted. Вся основная логика и рендер слотов безопасны для SSR.

Поддержка RTL

Оберните приложение (или только части, использующие эту библиотеку) в dir="rtl" — проп rtl нигде не существует, потому что он не нужен:

vue
<VirtualTable :columns="columns" :rows="rows" dir="rtl" style="height: 500px" />

Каждый компонент использует логические CSS-свойства (inset-inline-start/inset-inline-end, padding-inline-start, margin-inline-start) вместо физических left/right для позиционирования — отступ VirtualTree, зафиксированные столбцы VirtualTable и ручка изменения размера столбца, позиционирование ячеек VirtualGrid и горизонтальный ползунок VirtualScrollbar автоматически зеркалируются, когда браузер разрешает direction: rtl, без единого JS-ветвления по направлению.

Единственное место, реально нуждающееся в JS — это element.scrollLeft, чей знак/точка отсчёта различаются в RTL между браузерами (0 у начального края, отрицательное значение к концу, согласно современной спецификации). Виртуализация столбцов VirtualTable, горизонтальный режим VirtualScrollbar и раскладка horizontal у VirtualList — все читают/пишут его через normalizeScrollLeft / setNormalizedScrollLeft, а не через сырое свойство — обе функции также экспортируются из vue-virtual-scroller-kit, если вам нужна та же нормализация в собственном коде. Drag-to-resize столбца также инвертирует знак дельты указателя в RTL, поскольку там ручка изменения размера находится на физически левом краю.

ts
import { normalizeScrollLeft, setNormalizedScrollLeft } from 'vue-virtual-scroller-kit'

const distanceFromStart = normalizeScrollLeft(el) // работает одинаково в LTR и RTL
setNormalizedScrollLeft(el, distanceFromStart + 100)

Архитектура

vue-virtual-scroller-kit

├── PositionManager (дерево отрезков)
│     Обновления высот и запросы префиксных сумм за O(log n)

├── useVirtualScroll
│     itemCount + estimatedItemSize → visibleRange, totalHeight, scrollTo
│     ResizeObserver на контейнере прокрутки (изменение размера вьюпорта)
│     Пересчёт, батчируемый через RAF, debounced измерения строк
│     Reflow с компенсацией якоря: изменения высоты над вьюпортом сдвигают
│     scrollTop на ту же дельту, так что видимые строки никогда не прыгают
│     Опциональное отслеживание скорости → blurAmount (опция motionBlur)
│     Опциональная горизонтальная ось (scrollLeft/clientWidth, RTL-безопасно
│     через normalizeScrollLeft); фиксируется при монтировании, как pageMode

├── VirtualList
│     scrollElement / pageMode / прокрутка window
│     ResizeObserver на каждой видимой строке (динамические высоты)
│     Восстановление прокрутки через sessionStorage
│     Пул переиспользования DOM (проп recyclePool)
│     Опциональная горизонтальная раскладка (проп horizontal)

├── GroupedVirtualList
│     Разворачивает GroupDef[] → VirtualRow[] (заголовки + элементы)
│     Анимированный автомат состояний раскрытия/сворачивания на группу
│     Опциональный липкий оверлей заголовка (stickyGroupHeaders),
│     отслеживает группу на visibleRange.start — не настоящая CSS sticky-строка
│     (строки абсолютно позиционированы)
│     Основан на VirtualList

├── VirtualTable
│     Липкий заголовок, зафиксированные столбцы, сортировка, изменение
│     размера, переупорядочивание, виртуализация столбцов
│     Закреплённые верхние/нижние строки, встроенная ленивая загрузка
│     (onLoadMore / hasMore / isLoading)
│     Порядок столбцов (columnOrder) и видимость (hiddenColumnKeys)
│     отслеживаются внутренне, оба поверх пропа columns
│     Основан на VirtualList

├── VirtualGrid
│     Автоматическое число столбцов из ширины контейнера (ResizeObserver)
│     rowHeightWithGap передаётся как Ref в useVirtualScroll
│     Опциональный dynamicRowHeight: обёртка на строку (flex) + ResizeObserver,
│     высота строки = максимум по ячейкам этой строки, вместо индивидуально
│     абсолютно позиционированных ячеек фиксированной высоты
│     Основан напрямую на useVirtualScroll

├── VirtualTree
│     Рекурсивный flattenNodes с поддержкой ленивой загрузки
│     Основан на VirtualList

├── InfiniteLoader
│     Проверка порога при прокрутке (debounced 50 мс)
│     Сохранение позиции прокрутки при добавлении в начало
│     Основан на VirtualList

├── VirtualSelect
│     Клиентская фильтрация либо опциональный удалённый режим (debounced
│     событие search, слот isLoading), навигация с клавиатуры,
│     жизненный цикл открытия/закрытия
│     Основан на VirtualList

├── VirtualScrollbar
│     Отделён от useVirtualScroll — синхронизируется с любым
│     getScrollElement() через собственные слушатели scroll/ResizeObserver
│     Ползунок с pointer-drag + трек с переходом по клику

├── useVirtualKeyboardNav
│     Самостоятельный composable — keydown на target или document

├── useDraggableList
│     Pointer-события (без HTML5 Drag API)
│     Призрачный элемент через fixed-позиционирование + Teleport
│     Анимация зазора через translateY на соседях
│     Цикл авто-прокрутки на RAF при приближении к краям контейнера

├── useRowSelection
│     Не зависит от структуры данных (работает с VirtualList, VirtualTable,
│     обычными массивами)
│     Клик переключает на месте; Shift+клик заполняет диапазон от
│     последнего переключённого индекса (соглашение чекбоксов Gmail)

└── useVisibilityTracker
      observe()/unobserve() по ключу, на основе настоящего IntersectionObserver
      (не сравнение visibleRange — точность при overscan/частичных порогах)
      Root опрашивается и автоматически перестраивается, если разрешается
      с задержкой или меняется

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

В src/__bench__/ находятся бенчмарки Vitest (vitest bench, под капотом tinybench) для двух частей, несущих заявление пакета об O(log n): непосредственно дерево отрезков PositionManager и стоимость монтирования/перестройки useVirtualScroll поверх него. Запустите их сами:

bash
npm run bench

Бенчмаркинг — экспериментальная функция Vitest, цифры могут меняться между версиями Vitest. Это изолированные микробенчмарки в jsdom (монтирование/размонтирование компонента, без реального paint или layout), а не замена профилирования реального приложения. Относитесь к ним как к относительному подтверждению дизайна O(log n), а не как к абсолютным цифрам для вашего оборудования.

PositionManager — среднее время на операцию, одна машина разработчика, один прогон:

Элементов (n)construct (фикс. высота)findIndexset (resize)getOffset
10000.0125 мс0.0001 мс0.0001 мс0.0001 мс
10 0000.166 мс0.0002 мс0.0001 мс0.0002 мс
100 0000.612 мс0.0002 мс0.0002 мс0.0002 мс

construct — единственная операция O(n) (строит всё дерево один раз) — её стоимость растёт с n, как и ожидается. findIndex, set и getOffset — операции O(log n), запрашиваемые на каждое событие прокрутки и каждое измерение строки; их стоимость на вызов почти не меняется от 1000 до 100 000 элементов — это дерево отрезков делает свою работу.

useVirtualScroll — монтирование composable в реальном Vue-компоненте и перестройка его внутреннего PositionManager при изменении itemCount:

Элементов (n)МонтированиеИзменение itemCount (перестройка)
10000.204 мс0.206 мс
10 0000.352 мс0.373 мс
100 0001.51 мс2.22 мс

Оба масштабируются вместе со стоимостью построения PositionManager внутри них, плюс собственные накладные расходы Vue на монтирование/реактивность — всё ещё уверенно в диапазоне долей миллисекунды или единиц миллисекунд даже при 100 тыс. строк.

Размер бандла и peer-зависимости

Точка входаPeer-зависимостиПримечания
vue-virtual-scroller-kitvue ^3.3Полный бандл — все компоненты и composables

Пакет поставляется как tree-shakeable ESM (dist/index.js) + CJS (dist/index.cjs) двойная сборка. Импорт только VirtualList с неиспользуемыми VirtualTable, VirtualTree и т.д. приводит к тому, что эти модули отбрасываются бандлером.

Разработка

bash
npm install
npm run typecheck   # vue-tsc
npm run lint        # eslint
npm run lint:css    # stylelint
npm test            # vitest — модульные тесты (src/__tests__)
npm run demo        # запускает демо-приложение на http://localhost:5173

Сквозные (end-to-end) тесты управляют демо-приложением в реальном браузере (Playwright) и находятся в demo/e2e/ — именно они реально ловят баги, связанные с таймингом прокрутки и браузерными API, которые jsdom не может уловить (реальная анимация плавной прокрутки, реальный тайминг ResizeObserver/requestAnimationFrame, реальная RTL-раскладка):

bash
cd demo
npm install
npm run test:e2e

CI (.github/workflows/ci.yml) запускает typecheck, lint, модульные тесты и сборку на Node 20 и 22, плюс набор e2e-тестов, при каждом push и pull request.

Лицензия

MIT