Throttling и debouncing

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

При работе с mo.js это становится критичным, поскольку библиотека часто используется для построения интерактивных анимаций, завязанных на пользовательские действия и состояние интерфейса. Управление частотой вызовов обработчиков напрямую влияет на плавность анимаций и стабильность кадровой частоты.


Частые события и проблема избыточных вызовов

Браузер генерирует события с высокой скоростью:

  • mousemove — десятки и сотни раз в секунду
  • scroll — непрерывно во время прокрутки
  • resize — при изменении размера окна может вызываться многократно
  • input — при вводе текста на каждый символ

Если каждый вызов приводит к:

  • пересчёту DOM
  • запуску анимации
  • изменению состояния объектов mo.js
  • перерисовке сцены

то производительность резко падает.


Throttling: ограничение частоты выполнения

Throttling ограничивает выполнение функции так, чтобы она вызывалась не чаще заданного интервала времени.

Идея заключается в том, что при частых событиях функция выполняется строго раз в N миллисекунд, игнорируя промежуточные вызовы.

Базовая реализация throttling

function throttle(fn, delay) {
  let lastCall = 0;

  return function (...args) {
    const now = Date.now();

    if (now - lastCall >= delay) {
      lastCall = now;
      fn.apply(this, args);
    }
  };
}

Поведение

  • событие может вызываться 100 раз в секунду
  • обработчик выполнится, например, 1 раз в 200 мс
  • промежуточные вызовы игнорируются

Применение в анимациях

Throttling особенно полезен, когда:

  • обновление состояния влияет на запуск эффектов
  • требуется контролировать частоту создания новых анимаций
  • необходимо ограничить реакцию на scroll или mousemove

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

const handleScroll = throttle((event) => {
  const progress = window.scrollY / (document.body.scrollHeight - window.innerHeight);

  // условный запуск анимации в зависимости от прогресса
  if (progress > 0.5) {
    // запуск анимации mo.js
  }
}, 200);

window.addEventListener('scroll', handleScroll);

Debouncing: задержка выполнения до паузы событий

Debouncing обеспечивает выполнение функции только после того, как поток событий прекратился на заданное время.

В отличие от throttling, где выполнение происходит регулярно, debouncing ждёт «тишины» в событиях.

Базовая реализация debouncing

function debounce(fn, delay) {
  let timer = null;

  return function (...args) {
    clearTimeout(timer);

    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

Поведение

  • событие вызывается много раз подряд
  • таймер постоянно сбрасывается
  • функция выполняется только после паузы

Типичные сценарии

  • ввод текста
  • завершение resize окна
  • окончание прокрутки
  • финализация сложных жестов

Отличие throttling и debouncing

Throttling

  • ограничивает частоту выполнения
  • функция вызывается регулярно
  • подходит для потоковых событий

Debouncing

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

Связь с mo.js и анимационными системами

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

  • burst-анимаций
  • переходов UI-элементов
  • эффектов на кликах и hover
  • реакций на scroll-driven интерфейсы

При неконтролируемом связывании событий и анимаций возникают проблемы:

  • множественные пересоздания tween-объектов
  • лаги при высокой частоте событий
  • пересечение анимационных состояний
  • деградация FPS

Throttling в scroll-driven анимациях

При построении анимаций, зависящих от прокрутки, throttling ограничивает обновления прогресса.

const updateAnimation = throttle(() => {
  const scrollTop = window.scrollY;
  const maxScroll = document.body.scrollHeight - window.innerHeight;
  const progress = scrollTop / maxScroll;

  // управление состоянием анимации
  animation.setProgress(progress);
}, 16);

Значение 16ms приблизительно соответствует 60 FPS и позволяет синхронизировать логику с кадровой частотой.


Debouncing при resize и пересчётах сцены

Изменение размеров окна часто вызывает множественные события подряд. Debouncing позволяет запускать пересчёт сцены только после завершения изменения.

const handleResize = debounce(() => {
  const width = window.innerWidth;
  const height = window.innerHeight;

  // пересоздание или обновление параметров анимации
  scene.resize(width, height);
}, 300);

window.addEventListener('resize', handleResize);

Использование с mousemove и интерактивными эффектами

Событие mousemove генерирует поток координат, который может быть использован для интерактивных эффектов. Без ограничений это приводит к перегрузке логики.

Throttling для движения мыши

const handleMove = throttle((e) => {
  const x = e.clientX;
  const y = e.clientY;

  particleSystem.setPosition(x, y);
}, 10);

window.addEventListener('mousemove', handleMove);

Debouncing для завершения жеста

const endInteraction = debounce(() => {
  particleSystem.explode();
}, 150);

window.addEventListener('mousemove', endInteraction);

Совмещение throttling и debouncing

В сложных интерфейсах оба подхода используются одновременно:

  • throttling — для непрерывного обновления состояния
  • debouncing — для фиксации завершения взаимодействия

Пример логики:

const onM ove = throttle((e) => {
  updatePreview(e.clientX, e.clientY);
}, 16);

const onMove End = debounce(() => {
  finalizeAnimation();
}, 120);

window.addEventListener('mousemove', onMove);
window.addEventListener('mousemove', onMoveEnd);

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

Использование этих техник снижает нагрузку на:

  • main thread браузера
  • layout и repaint операции
  • пересчёт стилей
  • создание и уничтожение анимационных объектов

В связке с mo.js это позволяет:

  • стабилизировать FPS
  • уменьшить количество пересозданий эффектов
  • избежать гонок состояний
  • повысить отзывчивость интерфейса

Ошибки при использовании

Слишком маленький throttle-interval

Приводит к фактическому отсутствию ограничения и перегрузке.

Слишком большой debounce-delay

Создаёт ощущение задержки и «тормозов» интерфейса.

Неправильное сочетание с requestAnimationFrame

При использовании requestAnimationFrame дополнительный throttle может быть избыточным и ухудшать точность синхронизации.


Связь с requestAnimationFrame

В анимационных системах часто вместо throttling используют requestAnimationFrame, так как он синхронизирован с рендерингом браузера.

Однако:

  • throttling полезен для событий вне кадра (scroll, resize)
  • rAF полезен для циклических анимаций
  • debouncing полезен для финализации действий

Архитектурный подход в анимационных интерфейсах

Типичная схема взаимодействия:

  • события браузера → throttle/debounce слой
  • слой управления состоянием
  • система анимации (mo.js)
  • рендеринг кадра

Такой подход разделяет:

  • поток событий
  • бизнес-логику
  • визуальные эффекты

что обеспечивает предсказуемость и стабильность интерфейса.