Виртуальный скроллинг

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

При классическом рендеринге список из десятков тысяч элементов создаёт значительную нагрузку на браузер. Каждый элемент занимает память, участвует в layout-расчётах и вызывает перерасчёт стилей при изменениях.

Виртуальный скроллинг заменяет полный набор элементов фиксированным окном:

  • отображается только видимая часть списка
  • дополнительно поддерживается буфер сверху и снизу
  • остальные элементы существуют только в виде данных

Ключевым становится управление позиционированием контейнера, имитирующего полную высоту списка.

Структура виртуализированного контейнера

Базовая архитектура состоит из трёх уровней:

  • внешний контейнер со scroll
  • внутренний spacer, имитирующий полную высоту списка
  • рендер-окно с реальными DOM-элементами

Высота spacer рассчитывается как:

totalHeight = itemHeight * totalItems

или в случае переменной высоты — динамически, через кэширование измерений.

Реальные элементы позиционируются поверх spacer с помощью transform: translateY(...).

Расчёт видимого диапазона

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

startIndex = Math.floor(scrollTop / itemHeight)
endIndex = startIndex + visibleCount + buffer

Buffer добавляется для сглаживания прокрутки и предотвращения “пустых” областей при быстрых скроллах.

При переменной высоте элементов используется предварительная карта высот и бинарный поиск по накопленной сумме.

DOM recycling и переиспользование элементов

Основной оптимизацией является переиспользование DOM-узлов. Вместо создания новых элементов применяется пул:

  • фиксированное количество DOM-элементов
  • обновление их содержимого при изменении диапазона
  • перерасчёт позиции через transform

Это снижает количество операций layout и repaint.

Связь виртуального скроллинга с анимацией Velocity.js

Библиотека Velocity.js используется для плавного управления визуальным перемещением списка и отдельных элементов. Вместо прямого изменения scrollTop или мгновенного обновления transform применяется анимация, управляемая через Velocity.

Основные сценарии применения:

  • плавное догоняющее движение при инерционном скролле
  • сглаживание скачков при пересчёте диапазона
  • анимация появления/исчезновения элементов

Velocity работает быстрее классических jQuery-анимаций за счёт использования requestAnimationFrame и оптимизированного движка изменения CSS-свойств.

Пример интеграции смещения контейнера:

Velocity(container, {
  translateY: -offset
}, {
  duration: 120,
  easing: "easeOutQuad"
});

Синхронизация scrollTop и transform

Виртуальный скролл часто отделяет фактический scroll контейнера от визуального положения элементов. Возможны два подхода:

1. Нативный scroll + transform

scrollTop управляет диапазоном, а элементы смещаются через transform.

2. Полностью кастомный скролл

scrollTop игнорируется, вместо него используется внутреннее состояние:

let scrollPosition = 0;

container.addEventListener('wheel', (e) => {
  scrollPosition += e.deltaY;
  render(scrollPosition);
});

Velocity применяется для сглаживания переходов:

Velocity(state, { position: target }, {
  duration: 200,
  progress: () => render(state.position)
});

Буферизация и предотвращение “дёрганий”

При резкой прокрутке важно предотвращать частое пересоздание диапазона. Используются техники:

  • throttling scroll-событий
  • requestAnimationFrame для синхронизации рендера
  • расширенный buffer zone

Буфер обычно равен 2–3 экранам:

buffer = visibleCount * 2

Переменная высота элементов

Наиболее сложный сценарий связан с элементами разной высоты. Решение строится на:

  • измерении каждого элемента после рендера
  • хранении карты высот
  • вычислении offset через prefix sum

Структура кэша:

const heights = new Map();
const offsets = [];

Пересчёт offsets:

offsets[i] = offsets[i - 1] + heights[i - 1];

Velocity может использоваться для сглаживания изменения позиции при перерасчёте списка, предотвращая визуальные скачки.

Оптимизация layout thrashing

Частая ошибка виртуального скроллинга — чередование чтения и записи DOM в одном цикле, что вызывает layout thrashing.

Разделение фаз:

  1. чтение scrollTop и размеров
  2. вычисления индексов
  3. запись изменений DOM
  4. анимация Velocity

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

Использование transform вместо top/left

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

  • не вызывает reflow
  • работает через GPU-композитинг
  • лучше сочетается с Velocity

Пример:

.item {
  position: absolute;
  width: 100%;
  will-change: transform;
}
item.style.transform = `translateY(${y}px)`;

Сглаживание инерции прокрутки

Velocity позволяет эмулировать инерционный скролл, особенно в кастомных реализациях:

Velocity(scrollState, {
  velocity: 0
}, {
  duration: 500,
  easing: "easeOutSine"
});

Параллельно вычисляется текущая позиция и корректируется диапазон видимых элементов.

Управление пересозданием списка

При обновлении данных важно избегать полного rerender:

  • diff по индексам
  • обновление только изменившихся элементов
  • сохранение DOM-узлов

Структура обновления:

for (let i = start; i <= end; i++) {
  const el = pool[i - start];
  el.textContent = data[i];
}

Velocity применяется для анимации изменения содержимого:

Velocity(el, "fadeIn", { duration: 80 });

Производительность и узкие места

Основные факторы деградации:

  • частые измерения offsetHeight
  • неконтролируемые reflow при изменении DOM
  • отсутствие буферизации
  • чрезмерное использование JS-анимаций вместо transform

Оптимальный баланс достигается при:

  • минимальном числе DOM-элементов
  • полной зависимости от transform
  • ограничении частоты пересчётов
  • использовании Velocity только для визуальных эффектов, а не для основной логики скролла