Справочник
Типы TypeScript
Все публичные типы экспортируются из корня пакета:
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:
// ✗ Не работает для 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 нигде не существует, потому что он не нужен:
<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, поскольку там ручка изменения размера находится на физически левом краю.
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 поверх него. Запустите их сами:
npm run benchБенчмаркинг — экспериментальная функция Vitest, цифры могут меняться между версиями Vitest. Это изолированные микробенчмарки в jsdom (монтирование/размонтирование компонента, без реального paint или layout), а не замена профилирования реального приложения. Относитесь к ним как к относительному подтверждению дизайна O(log n), а не как к абсолютным цифрам для вашего оборудования.
PositionManager — среднее время на операцию, одна машина разработчика, один прогон:
| Элементов (n) | construct (фикс. высота) | findIndex | set (resize) | getOffset |
|---|---|---|---|---|
| 1000 | 0.0125 мс | 0.0001 мс | 0.0001 мс | 0.0001 мс |
| 10 000 | 0.166 мс | 0.0002 мс | 0.0001 мс | 0.0002 мс |
| 100 000 | 0.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 (перестройка) |
|---|---|---|
| 1000 | 0.204 мс | 0.206 мс |
| 10 000 | 0.352 мс | 0.373 мс |
| 100 000 | 1.51 мс | 2.22 мс |
Оба масштабируются вместе со стоимостью построения PositionManager внутри них, плюс собственные накладные расходы Vue на монтирование/реактивность — всё ещё уверенно в диапазоне долей миллисекунды или единиц миллисекунд даже при 100 тыс. строк.
Размер бандла и peer-зависимости
| Точка входа | Peer-зависимости | Примечания |
|---|---|---|
vue-virtual-scroller-kit | vue ^3.3 | Полный бандл — все компоненты и composables |
Пакет поставляется как tree-shakeable ESM (dist/index.js) + CJS (dist/index.cjs) двойная сборка. Импорт только VirtualList с неиспользуемыми VirtualTable, VirtualTree и т.д. приводит к тому, что эти модули отбрасываются бандлером.
Разработка
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-раскладка):
cd demo
npm install
npm run test:e2eCI (.github/workflows/ci.yml) запускает typecheck, lint, модульные тесты и сборку на Node 20 и 22, плюс набор e2e-тестов, при каждом push и pull request.
Лицензия
MIT