Skip to content

Справочник ​

Архитектура ​

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-kitvue >=3.3.0Полный бандл — все компоненты и 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.

Бенчмарки ​

В 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 тыс. строк.

Лицензия ​

MIT