Справочник
Архитектура
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)
│ containerRole/itemRole — переопределяемые ARIA-роли, позволяющие
│ оборачивающему компоненту (VirtualTree, VirtualSelect) подавить
│ list/listitem и самому владеть деревом доступности
│
├── GroupedVirtualList
│ Разворачивает GroupDef[] → VirtualRow[] (заголовки + элементы)
│ Анимированный автомат состояний раскрытия/сворачивания на группу
│ Опциональный липкий оверлей заголовка (stickyGroupHeaders),
│ отслеживает группу на visibleRange.start — не настоящая CSS sticky-строка
│ (строки абсолютно позиционированы)
│ Отдельная оценка размера по типу строки (estimatedItemSize против
│ estimatedGroupHeaderSize)
│ Основан на VirtualList
│
├── VirtualTable
│ Липкий заголовок, зафиксированные столбцы, сортировка, изменение
│ размера, переупорядочивание, виртуализация столбцов
│ Закреплённые верхние/нижние строки, встроенная ленивая загрузка
│ (onLoadMore / hasMore / isLoading)
│ Порядок столбцов (columnOrder) и видимость (hiddenColumnKeys)
│ отслеживаются внутренне, оба поверх пропа columns
│ Основан на VirtualList
│
├── VirtualGrid
│ Автоматическое число столбцов из ширины контейнера (ResizeObserver)
│ rowHeightWithGap передаётся как Ref в useVirtualScroll
│ Опциональный dynamicRowHeight: обёртка на строку (flex) + ResizeObserver,
│ высота строки = максимум по ячейкам этой строки, вместо индивидуально
│ абсолютно позиционированных ячеек фиксированной высоты
│ containerRole/rowRole/itemRole — переопределяемые ARIA-роли
│ (по умолчанию grid/row/gridcell), работающие так же, как
│ containerRole/itemRole у VirtualList
│ Основан напрямую на useVirtualScroll
│
├── VirtualTree
│ Рекурсивный flattenNodes с поддержкой ленивой загрузки
│ container-role="tree" / item-role="none" на внутреннем VirtualList,
│ чтобы собственные строки role="treeitem" компоновались корректно
│ Основан на VirtualList
│
├── InfiniteLoader
│ Проверка порога при прокрутке (debounced 50 мс)
│ Сохранение позиции прокрутки при добавлении в начало
│ Основан на VirtualList
│
├── VirtualSelect
│ Клиентская фильтрация либо опциональный удалённый режим (debounced
│ событие search, слот isLoading), навигация с клавиатуры,
│ жизненный цикл открытия/закрытия
│ container-role="none" / item-role="none" на внутреннем VirtualList,
│ чтобы собственные role="listbox"/role="option" выпадающего списка
│ компоновались корректно
│ Основан на VirtualList
│
├── VirtualScrollbar
│ Отделён от useVirtualScroll — синхронизируется с любым
│ getScrollElement() через собственные слушатели scroll/ResizeObserver
│ Ползунок с pointer-drag + трек с переходом по клику
│ Ползунок role="scrollbar" с полной поддержкой клавиатуры (см. Доступность)
│
├── useVirtualKeyboardNav
│ Самостоятельный composable — keydown на target или document
│
├── useDraggableList
│ Только pointer-события (без HTML5 Drag API — без нативного
│ атрибута draggable)
│ Призрачный элемент через fixed-позиционирование + Teleport
│ Анимация зазора через translateY на соседях
│ Цикл авто-прокрутки на RAF при приближении к краям контейнера
│
├── useRowSelection
│ Не зависит от структуры данных (работает с VirtualList, VirtualTable,
│ обычными массивами)
│ Клик переключает на месте; Shift+клик заполняет диапазон от
│ последнего переключённого индекса (соглашение чекбоксов Gmail)
│
└── useVisibilityTracker
observe()/unobserve() по ключу, на основе настоящего IntersectionObserver
(не сравнение visibleRange — точность при overscan/частичных порогах)
Root опрашивается и автоматически перестраивается, если разрешается
с задержкой или меняетсяПоддержка SSR
Все компоненты рендерят первые ssrPreloadCount строк (по умолчанию 20) на сервере с оценочными высотами. Клиентская гидратация постепенно заменяет оценочные позиции измеренными высотами через ResizeObserver — без сдвига разметки и прыжков прокрутки.
Компоненты, использующие API, доступные только в браузере (ResizeObserver, IntersectionObserver, requestAnimationFrame, window.scroll), защищают эти вызовы проверкой typeof window !== 'undefined' и onMounted. Вся основная логика и рендер слотов безопасны для SSR.
Доступность
| Возможность | Реализация |
|---|---|
role="list" / role="listitem" | Применяется к контейнеру VirtualList и каждой видимой строке (переопределяется через containerRole/itemRole) |
aria-rowcount | Устанавливается в общее число элементов на контейнере списка |
aria-rowindex | Устанавливается в index + 1 на каждой видимой строке |
aria-busy | Устанавливается в "true" на контейнере, пока isLoading истинно |
role="grid" / role="row" / role="gridcell" | Используется в VirtualGrid (переопределяется через containerRole/rowRole/itemRole) — каждая строка ячеек обёрнута в role="row" |
aria-rowcount / aria-colcount | Устанавливается на контейнере VirtualGrid, только пока containerRole остаётся в семействе grid/table |
aria-rowindex / aria-colindex | Устанавливается на строках и ячейках VirtualGrid, только пока rowRole/itemRole остаются в семействе row/cell |
role="tree" / role="treeitem" | Используется в VirtualTree — внутренний VirtualList подавляет собственные роли list/listitem через containerRole="tree"/itemRole="none", чтобы дерево компоновалось без лишнего уровня вложенности |
aria-expanded / aria-level | Устанавливается на строках дерева с дочерними элементами |
role="combobox" / role="listbox" / role="option" | Используется в VirtualSelect — внутренний VirtualList подавляет собственные роли так же, как VirtualTree |
aria-expanded / aria-haspopup / aria-selected | Устанавливается на триггере select и опциях |
role="scrollbar" | Используется на ползунке VirtualScrollbar |
aria-orientation / aria-valuemin / aria-valuemax / aria-valuenow | Устанавливается на ползунке VirtualScrollbar, отражая текущую позицию прокрутки |
Поддержка клавиатуры (VirtualScrollbar) | Стрелки сдвигают позицию, Home/End прыгают к началу/концу, PageUp/PageDown — на размер вьюпорта |
aria-sort | Устанавливается на сортируемых заголовках VirtualTable ("ascending", "descending" или "none") |
| Поддержка клавиатуры (списки) | Полная навигация с клавиатуры через useVirtualKeyboardNav |
Размер бандла и peer-зависимости
| Точка входа | Peer-зависимости | Примечания |
|---|---|---|
vue-virtual-scroller-kit | vue >=3.3.0 | Полный бандл — все компоненты и 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.
Бенчмарки
В 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 тыс. строк.
Лицензия
MIT