Виртуализация списка в Slim Select представляет собой подход к рендерингу элементов выпадающего списка, при котором в DOM отображается только ограниченное количество узлов, соответствующих текущей области видимости, а остальные элементы подгружаются и отрисовываются динамически по мере прокрутки. Такой механизм критичен для сценариев, где количество опций измеряется сотнями или тысячами, поскольку стандартная отрисовка всех элементов приводит к деградации производительности, увеличению времени открытия списка и росту потребления памяти.
Обычный HTML-select или его кастомные реализации на базе JavaScript создают отдельный DOM-элемент для каждой опции. При небольшом объёме данных это не вызывает проблем, однако при увеличении количества элементов возникают системные ограничения:
Slim Select в своей базовой архитектуре минимизирует сложность DOM по сравнению с классическими кастомными селектами, но при больших наборах данных проблема остаётся актуальной, особенно если список содержит сложные шаблоны отображения или динамическую фильтрацию.
Основная идея виртуализации заключается в поддержании «окна» активных элементов, которые соответствуют текущему положению скролла. Вместо рендера всего массива данных используется подмножество:
Модель можно описать как функцию отображения:
f(x)= x
где x — позиция прокрутки, а результат — набор
элементов, подлежащих рендеру.
Типичная реализация виртуализации в контексте Slim Select строится вокруг нескольких слоёв:
1. Data layer (данные) Хранит полный массив опций. Это может быть массив объектов:
2. Viewport layer (окно отображения) Определяет текущий диапазон видимых элементов на основе:
3. Render layer (DOM-отрисовка) Создаёт только те элементы, которые попадают в вычисленный диапазон.
4. Scroll controller (контроллер прокрутки) Отслеживает события scroll и инициирует перерасчёт окна.
Ключевой алгоритм виртуализации сводится к вычислению startIndex и endIndex.
Пусть:
h — высота одного элемента;H — высота контейнера;s — scrollTop;n — общее число элементов.Тогда:
startIndex = floor(s / h)endIndex = startIndex + ceil(H / h)С учётом буфера:
startIndex = max(0, startIndex - buffer)endIndex = min(n, endIndex + buffer)=(0,-b)=(n,+b)
где b — размер буфера.
Буферизация уменьшает эффект «дёргания» при прокрутке и предотвращает появление пустых зон при быстром скролле.
Slim Select изначально ориентирован на лёгкую кастомизацию и работу с DOM-структурой выпадающего списка. Виртуализация в таком контексте обычно реализуется не как встроенная функция, а как расширение поведения через перехват рендеринга списка.
Типовой подход включает:
Ключевая идея — отделить данные от представления, оставив Slim Select управление состоянием выбора, а виртуализацию — слоем отображения.
class VirtualList {
constructor({ items, container, itemHeight = 32, buffer = 5 }) {
this.items = items;
this.container = container;
this.itemHeight = itemHeight;
this.buffer = buffer;
this.scrollTop = 0;
this.viewportHeight = container.clientHeight;
this.container.addEventListener("scroll", () => this.onScroll());
this.render();
}
onScroll() {
this.scrollTop = this.container.scrollTop;
this.render();
}
getVisibleRange() {
const start = Math.max(
0,
Math.floor(this.scrollTop / this.itemHeight) - this.buffer
);
const end = Math.min(
this.items.length,
Math.ceil((this.scrollTop + this.viewportHeight) / this.itemHeight) + this.buffer
);
return { start, end };
}
render() {
const { start, end } = this.getVisibleRange();
const fragment = document.createDocumentFragment();
const offsetTop = start * this.itemHeight;
const spacerTop = document.createElement("div");
spacerTop.style.height = offsetTop + "px";
fragment.appendChild(spacerTop);
for (let i = start; i < end; i++) {
const el = document.createElement("div");
el.className = "ss-option";
el.textContent = this.items[i].label;
el.dataset.index = i;
fragment.appendChild(el);
}
const spacerBottom = document.createElement("div");
spacerBottom.style.height =
(this.items.length - end) * this.itemHeight + "px";
fragment.appendChild(spacerBottom);
this.container.innerHTML = "";
this.container.appendChild(fragment);
}
}
При интеграции виртуализации важно учитывать внутреннюю логику Slim Select:
Виртуализация должна работать поверх фильтрованного массива, а не исходного, иначе нарушается логика поиска.
Фильтрация обычно выполняется до передачи данных в виртуальный слой:
const filtered = items.filter(item =>
item.label.toLowerCase().includes(query.toLowerCase())
);
Далее виртуализатор работает только с filtered.
Один из сложных аспектов — предположение о фиксированной высоте элемента. Slim Select может использовать кастомный рендер, где высота опции варьируется (например, длинный текст, иконки, группы).
В таких случаях используются альтернативные стратегии:
1. Приближённая модель
2. Dynamic measurement
3. Hybrid rendering
При интенсивной прокрутке возникает проблема частых re-render операций. Решение:
Оптимизированный обработчик:
onScroll() {
if (this.ticking) return;
this.ticking = true;
requestAnimationFrame(() => {
this.scrollTop = this.container.scrollTop;
this.render();
this.ticking = false;
});
}
Виртуализация не должна влиять на сохранение выбранных значений. Slim Select хранит selected state отдельно от DOM, поэтому при пересоздании элементов необходимо:
Иначе при скролле возможно «исчезновение» выбранного состояния, если элемент вышел из DOM.
При использовании виртуализации важно сохранять корректную семантику:
role="listbox" для контейнера;role="option" для элементов;aria-activedescendant для активного элемента;aria-setsize и
aria-posinset.Проблема возникает из-за того, что не все элементы находятся в DOM
одновременно, поэтому значения posinset должны вычисляться
относительно общего массива, а не текущего окна.
Виртуализация становится оправданной при:
При малых объёмах данных виртуализация может, наоборот, добавлять лишнюю сложность:
Slim Select часто используется с встроенным поиском, который динамически фильтрует список. Виртуализация должна реагировать на изменение query:
Иначе возможны артефакты, когда scroll указывает на старый индекс, отсутствующий в новом наборе.
Дополнительная оптимизация заключается в повторном использовании DOM-элементов:
Это особенно важно при списках с частыми обновлениями (например, live search).
Поведение системы можно описать как непрерывный цикл:
Такая структура позволяет сохранять стабильную производительность даже при значительных объёмах данных, сохраняя при этом предсказуемое поведение интерфейса и совместимость с архитектурой Slim Select.