Виртуализация при необходимости

В библиотеке Shopify Draggable виртуализация элементов применяется для оптимизации работы с большим количеством draggable-объектов, когда непосредственная отрисовка и управление всеми DOM-элементами одновременно становится неэффективной. Виртуализация позволяет поддерживать плавное взаимодействие пользователя с интерфейсом, уменьшая нагрузку на браузер и улучшая отзывчивость.


Принципы работы виртуализации

Основная идея виртуализации заключается в том, чтобы отображать в DOM только те элементы, которые находятся в видимой области, и динамически добавлять или удалять их при прокрутке. В контексте Draggable это особенно важно для списков или сеток с сотнями или тысячами элементов:

  • Lazy-rendering: элементы создаются по мере необходимости, а не все сразу.
  • Windowing: визуальная область ограничивается «окном», в котором активно происходит drag & drop.
  • Event delegation: обработчики событий назначаются не на каждый элемент, а на контейнер, что снижает затраты на память и CPU.

Настройка виртуализации в Draggable

Draggable не имеет встроенного полного механизма виртуализации, как специализированные библиотеки (например, React Virtualized), но предоставляет достаточную гибкость для её интеграции.

Основные подходы:

  1. Использование контейнера с ограниченной высотой и overflow:
const container = document.querySelector('.draggable-container');
container.style.height = '500px';
container.style.overflow = 'auto';

Это позволяет следить за скроллом и динамически подгружать видимые элементы.

  1. Динамическое создание элементов при скролле:
container.addEventListener('scroll', () => {
  const scrollTop = container.scrollTop;
  const containerHeight = container.clientHeight;

  // Рассчитываем индекс первого и последнего видимого элемента
  const startIndex = Math.floor(scrollTop / itemHeight);
  const endIndex = Math.ceil((scrollTop + containerHeight) / itemHeight);

  renderVisibleItems(startIndex, endIndex);
});

Здесь itemHeight — фиксированная высота элемента списка. Функция renderVisibleItems отвечает за добавление DOM-элементов только для диапазона [startIndex, endIndex].

  1. Интеграция с Draggable:
const draggable = new Draggable(container, {
  draggable: '.draggable-item',
  delay: 0,
  mirror: {
    constrainDimensions: true
  }
});

При динамической подгрузке элементов необходимо перерегистрировать новые элементы в Draggable, чтобы они становились draggable:

function renderVisibleItems(start, end) {
  container.innerHTML = ''; // очищаем предыдущие
  for (let i = start; i < end; i++) {
    const item = createItem(i);
    container.appendChild(item);
  }
  draggable.destroy(); // удаляем старую инициализацию
  draggable.add(container.querySelectorAll('.draggable-item')); // добавляем новые элементы
}

Оптимизация виртуализации

Для больших списков важно избегать перерегистрации Draggable на каждый скролл. Оптимальные методы:

  • Кэширование элементов: хранение заранее созданных элементов и их повторное использование вместо постоянного удаления и добавления.
  • Буфер вокруг видимой области: рендерить несколько элементов выше и ниже видимой области, чтобы drag & drop был плавным при быстром скролле.
  • Throttle/ Debounce скролла: сокращает количество вызовов функции рендера, снижая нагрузку на браузер:
let timeout;
container.addEventListener('scroll', () => {
  clearTimeout(timeout);
  timeout = setTimeout(() => {
    updateVisibleItems();
  }, 50);
});

Особенности работы drag & drop с виртуализированными элементами

  • Mirror и placeholder создаются Draggable в момент начала drag. При виртуализации важно, чтобы элемент, который будет перемещаться, не исчезал из DOM во время drag, иначе mirror перестанет корректно отображаться.
  • Позиционирование элементов: рекомендуется использовать абсолютное позиционирование внутри контейнера, чтобы динамическое добавление и удаление элементов не меняло layout, нарушая порядок drag & drop.
  • События drag:over и drag:out должны корректно работать даже при частичной подгрузке элементов. Это можно обеспечить через delegation: назначение обработчиков на контейнер, а не на отдельные элементы.

Пример интеграции с virtualized list

const container = document.querySelector('.draggable-container');
const items = Array.from({ length: 1000 }, (_, i) => i);
const itemHeight = 50;

function createItem(index) {
  const div = document.createElement('div');
  div.className = 'draggable-item';
  div.style.height = `${itemHeight}px`;
  div.textContent = `Элемент ${index}`;
  return div;
}

function renderVisibleItems(start, end) {
  container.innerHTML = '';
  for (let i = start; i < end; i++) {
    container.appendChild(createItem(items[i]));
  }
  draggable.destroy();
  draggable.add(container.querySelectorAll('.draggable-item'));
}

const draggable = new Draggable(container, { draggable: '.draggable-item' });

container.addEventListener('scroll', () => {
  const startIndex = Math.floor(container.scrollTop / itemHeight);
  const endIndex = startIndex + Math.ceil(container.clientHeight / itemHeight) + 2; // буфер
  renderVisibleItems(startIndex, endIndex);
});

renderVisibleItems(0, Math.ceil(container.clientHeight / itemHeight) + 2);

Такой подход обеспечивает плавную работу drag & drop на тысячах элементов, минимизируя нагрузку на DOM и повышая производительность интерфейса.