Виртуализация списка

Виртуализация списка в Slim Select представляет собой подход к рендерингу элементов выпадающего списка, при котором в DOM отображается только ограниченное количество узлов, соответствующих текущей области видимости, а остальные элементы подгружаются и отрисовываются динамически по мере прокрутки. Такой механизм критичен для сценариев, где количество опций измеряется сотнями или тысячами, поскольку стандартная отрисовка всех элементов приводит к деградации производительности, увеличению времени открытия списка и росту потребления памяти.

Обычный HTML-select или его кастомные реализации на базе JavaScript создают отдельный DOM-элемент для каждой опции. При небольшом объёме данных это не вызывает проблем, однако при увеличении количества элементов возникают системные ограничения:

  • резкое увеличение времени первичного рендера;
  • задержка при открытии dropdown;
  • высокая нагрузка на layout и reflow;
  • рост затрат на обработку событий (hover, click);
  • ухудшение отзывчивости прокрутки.

Slim Select в своей базовой архитектуре минимизирует сложность DOM по сравнению с классическими кастомными селектами, но при больших наборах данных проблема остаётся актуальной, особенно если список содержит сложные шаблоны отображения или динамическую фильтрацию.

Принцип виртуализации: окно видимости

Основная идея виртуализации заключается в поддержании «окна» активных элементов, которые соответствуют текущему положению скролла. Вместо рендера всего массива данных используется подмножество:

  • отображаются только элементы в диапазоне видимых индексов;
  • дополнительные элементы подгружаются при изменении scrollTop;
  • DOM-пул переиспользуется для снижения затрат на создание узлов.

Модель можно описать как функцию отображения:

f(x)= x

где x — позиция прокрутки, а результат — набор элементов, подлежащих рендеру.

Архитектура виртуализированного списка

Типичная реализация виртуализации в контексте Slim Select строится вокруг нескольких слоёв:

1. Data layer (данные) Хранит полный массив опций. Это может быть массив объектов:

  • id
  • label
  • value
  • disabled
  • metadata

2. Viewport layer (окно отображения) Определяет текущий диапазон видимых элементов на основе:

  • высоты контейнера;
  • высоты одного элемента;
  • scrollTop;
  • buffer zone (запас элементов сверху и снизу).

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

Slim Select изначально ориентирован на лёгкую кастомизацию и работу с DOM-структурой выпадающего списка. Виртуализация в таком контексте обычно реализуется не как встроенная функция, а как расширение поведения через перехват рендеринга списка.

Типовой подход включает:

  • замещение стандартного рендера списка;
  • подмену контейнера options;
  • внедрение собственного scroll handler;
  • контроль количества создаваемых 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

При интеграции виртуализации важно учитывать внутреннюю логику Slim Select:

  • управление состоянием выбранных значений;
  • синхронизация input и dropdown;
  • обработка поиска (search filtering);
  • обновление списка при изменении данных.

Виртуализация должна работать поверх фильтрованного массива, а не исходного, иначе нарушается логика поиска.

Фильтрация обычно выполняется до передачи данных в виртуальный слой:

const filtered = items.filter(item =>
  item.label.toLowerCase().includes(query.toLowerCase())
);

Далее виртуализатор работает только с filtered.

Проблемы точности и пересчёта высоты

Один из сложных аспектов — предположение о фиксированной высоте элемента. Slim Select может использовать кастомный рендер, где высота опции варьируется (например, длинный текст, иконки, группы).

В таких случаях используются альтернативные стратегии:

1. Приближённая модель

  • фиксированная средняя высота;
  • корректировка после рендера.

2. Dynamic measurement

  • измерение реальной высоты первого батча;
  • перерасчёт виртуальных границ.

3. Hybrid rendering

  • фиксированные элементы виртуализируются;
  • нестандартные группы рендерятся полностью.

Поведение при быстрых изменениях scroll

При интенсивной прокрутке возникает проблема частых re-render операций. Решение:

  • debounce scroll handler;
  • requestAnimationFrame для синхронизации;
  • кеширование диапазонов.

Оптимизированный обработчик:

onScroll() {
  if (this.ticking) return;

  this.ticking = true;

  requestAnimationFrame(() => {
    this.scrollTop = this.container.scrollTop;
    this.render();
    this.ticking = false;
  });
}

Состояние выбранных элементов

Виртуализация не должна влиять на сохранение выбранных значений. Slim Select хранит selected state отдельно от DOM, поэтому при пересоздании элементов необходимо:

  • повторно применять selected классы;
  • синхронизировать aria-selected;
  • сохранять активный index.

Иначе при скролле возможно «исчезновение» выбранного состояния, если элемент вышел из DOM.

Доступность и ARIA при виртуализации

При использовании виртуализации важно сохранять корректную семантику:

  • role="listbox" для контейнера;
  • role="option" для элементов;
  • aria-activedescendant для активного элемента;
  • обновление aria-setsize и aria-posinset.

Проблема возникает из-за того, что не все элементы находятся в DOM одновременно, поэтому значения posinset должны вычисляться относительно общего массива, а не текущего окна.

Производительность и границы применения

Виртуализация становится оправданной при:

  • 500+ элементов в списке;
  • сложной кастомной отрисовке опций;
  • частом использовании поиска;
  • мобильных устройствах с ограниченными ресурсами.

При малых объёмах данных виртуализация может, наоборот, добавлять лишнюю сложность:

  • дополнительные вычисления;
  • усложнение логики рендера;
  • потенциальные баги синхронизации.

Синхронизация с поиском Slim Select

Slim Select часто используется с встроенным поиском, который динамически фильтрует список. Виртуализация должна реагировать на изменение query:

  • пересчитывать items;
  • сбрасывать scrollTop;
  • переинициализировать диапазон.

Иначе возможны артефакты, когда scroll указывает на старый индекс, отсутствующий в новом наборе.

Управление памятью и переиспользование узлов

Дополнительная оптимизация заключается в повторном использовании DOM-элементов:

  • пул элементов вместо постоянного создания;
  • минимизация innerHTML;
  • обновление textContent вместо пересоздания node.

Это особенно важно при списках с частыми обновлениями (например, live search).

Итоговая модель поведения виртуализированного списка

Поведение системы можно описать как непрерывный цикл:

  • входные данные → фильтрация → вычисление окна → рендер → скролл-событие → обновление окна → повторный рендер

Такая структура позволяет сохранять стабильную производительность даже при значительных объёмах данных, сохраняя при этом предсказуемое поведение интерфейса и совместимость с архитектурой Slim Select.