Работа с большими списками

Основные принципы

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

  1. Минимизация DOM-операций – каждая операция над элементами списка должна быть максимально легкой. Избегать частого пересоздания элементов или лишнего рендеринга.
  2. Использование виртуализации – отображать в DOM только те элементы, которые видимы пользователю, подгружая остальные по мере скролла.
  3. Оптимизация событий Draggable – события drag:start, drag:move, drag:stop должны выполнять минимум вычислений.

Создание Draggable для большого списка

Простейшая инициализация Draggable для списка с сотнями элементов:

import { Draggable } from '@shopify/draggable';

const container = document.querySelector('.list-container');

const draggable = new Draggable(container, {
  draggable: '.list-item',
  delay: 0,
  mirror: {
    constrainDimensions: true
  }
});

draggable.on('drag:start', () => {
  container.classList.add('dragging-active');
});

draggable.on('drag:stop', () => {
  container.classList.remove('dragging-active');
});

Ключевые моменты:

  • draggable указывает на селектор элементов, которые можно перетаскивать.
  • mirror.constrainDimensions помогает предотвратить визуальные скачки зеркального элемента при больших списках.
  • События drag:start и drag:stop должны ограничиваться только необходимыми действиями для минимизации нагрузки.

Виртуализация элементов

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

function renderVisibleItems(items, scrollTop, containerHeight, itemHeight) {
  const startIndex = Math.floor(scrollTop / itemHeight);
  const endIndex = Math.min(items.length, Math.ceil((scrollTop + containerHeight) / itemHeight));
  const fragment = document.createDocumentFragment();

  for (let i = startIndex; i < endIndex; i++) {
    const item = document.createElement('div');
    item.className = 'list-item';
    item.textContent = items[i].name;
    fragment.appendChild(item);
  }

  const container = document.querySelector('.list-container');
  container.innerHTML = '';
  container.appendChild(fragment);
}

Советы по интеграции с Draggable:

  • Обновлять Draggable после рендеринга видимых элементов с помощью метода draggable.destroy() и повторной инициализации.
  • Можно использовать делегирование событий вместо привязки Draggable к каждому элементу отдельно, чтобы снизить нагрузку на память.

Делегирование событий

Draggable поддерживает делегирование, что особенно важно для длинных списков:

const draggable = new Draggable(document.body, {
  draggable: '.list-item',
  handle: '.drag-handle',
  delay: 100,
  mirror: {
    constrainDimensions: true
  }
});

Преимущества делегирования:

  • Элементы могут динамически добавляться или удаляться без необходимости пересоздавать весь Draggable.
  • Значительно снижается нагрузка на DOM при больших списках.

Ограничение области перетаскивания

Для улучшения UX при больших списках стоит ограничивать область, в пределах которой можно перемещать элементы:

const draggable = new Draggable(container, {
  draggable: '.list-item',
  mirror: {
    constrainDimensions: true,
    appendTo: container
  }
});

Это предотвращает «выскальзывание» элементов за пределы контейнера и снижает количество лишних перерисовок браузера.

Отслеживание изменений списка

Для больших списков крайне важно отслеживать изменения порядка элементов без постоянного обращения к DOM:

draggable.on('drag:stop', (event) => {
  const newIndex = Array.from(container.children).indexOf(event.source);
  const movedItem = items.splice(event.oldIndex, 1)[0];
  items.splice(newIndex, 0, movedItem);
});
  • event.oldIndex и event.source позволяют обновлять массив данных напрямую, не вызывая полное перерендеривание.
  • Такой подход обеспечивает согласованность данных и UI, особенно при асинхронной подгрузке элементов.

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

Для списков более 500–1000 элементов рекомендуется:

  • Ограничивать количество одновременно видимых элементов с помощью виртуализации.
  • Минимизировать CSS-анимации на каждом элементе во время перетаскивания.
  • Использовать requestAnimationFrame для сложных вычислений при событии drag:move.
  • Выключать ненужные события Draggable, такие как drag:over или drag:out, если они не используются.

Взаимодействие с динамическими данными

При подгрузке данных через API или добавлении элементов на лету важно:

  • Обновлять Draggable через draggable.destroy() + new Draggable(...) или использовать делегирование.
  • Поддерживать массив данных синхронизированным с DOM, чтобы любые действия пользователя корректно отражались в интерфейсе и данных.

Пример комплексной интеграции

const items = Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Item ${i}` }));
const container = document.querySelector('.list-container');

function render() {
  const fragment = document.createDocumentFragment();
  items.slice(0, 50).forEach(item => {
    const div = document.createElement('div');
    div.className = 'list-item';
    div.textContent = item.name;
    fragment.appendChild(div);
  });
  container.innerHTML = '';
  container.appendChild(fragment);
}

render();

const draggable = new Draggable(container, {
  draggable: '.list-item',
  mirror: { constrainDimensions: true },
  delay: 50
});

draggable.on('drag:stop', (evt) => {
  const oldIndex = Array.from(container.children).indexOf(evt.source);
  const moved = items.splice(oldIndex, 1)[0];
  const newIndex = Math.min(oldIndex, items.length);
  items.splice(newIndex, 0, moved);
});

Такой подход сочетает виртуализацию, делегирование и минимизацию операций с DOM, что обеспечивает плавную работу Draggable даже на очень больших списках.