Throttle и debounce

Throttle — ограничение частоты выполнения функции, при котором вызовы происходят не чаще заданного интервала времени. В контексте анимаций и событий браузера это один из ключевых инструментов управления производительностью, особенно при работе с высокочастотными событиями вроде scroll, resize, pointermove.

При использовании Motion One и других библиотек, основанных на Web Animations API, проблема избыточных вызовов проявляется особенно остро: каждый лишний пересчёт стилей или обновление прогресса анимации увеличивает нагрузку на main thread, что приводит к пропускам кадров и нестабильной визуальной плавности.

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

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

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

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

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

Такой подход используется в обработчиках событий, где важно поддерживать регулярное обновление состояния, но без перегрузки:

window.addEventListener(
  "scroll",
  throttle(() => {
    console.log(window.scrollY);
  }, 100)
);

В анимационных сценариях throttle применяется для:

  • обновления прогресса scroll-driven анимаций
  • пересчёта позиции элементов при прокрутке
  • обработки pointermove при drag-анимациях
  • синхронизации кастомных таймлайнов

Throttle и requestAnimationFrame

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

function rafThrottle(fn) {
  let ticking = false;
  let lastArgs;

  return function (...args) {
    lastArgs = args;

    if (!ticking) {
      ticking = true;

      requestAnimationFrame(() => {
        fn.apply(this, lastArgs);
        ticking = false;
      });
    }
  };
}

Такой механизм особенно полезен при интеграции с Motion One, где обновление параметров анимации должно происходить строго один раз за кадр.


Debounce (отложенное выполнение)

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

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

function debounce(fn, delay) {
  let timer;

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

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

Использование:

window.addEventListener(
  "resize",
  debounce(() => {
    console.log("resize stabilized");
  }, 200)
);

Debounce критичен в случаях, когда промежуточные значения не имеют смысла:

  • изменение размеров окна
  • ввод текста в поля поиска
  • завершение drag-and-drop операции
  • пересчёт layout после серии изменений DOM

Debounce в анимационных сценариях

При работе с Motion One debounce применяется для ситуаций, где анимация зависит от итогового состояния, а не промежуточных изменений.

Пример: пересчёт позиции элементов после изменения layout:

window.addEventListener(
  "resize",
  debounce(() => {
    recomputeLayout();
    animateElements();
  }, 250)
);

Различие поведения в контексте анимации

Throttle и debounce решают разные задачи управления потоком событий:

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

В анимационных системах это приводит к принципиально разным эффектам:

Throttle:

  • плавное обновление прогресса
  • постоянная синхронизация с движением пользователя
  • предсказуемая нагрузка на render pipeline

Debounce:

  • единичный пересчёт после серии изменений
  • оптимизация тяжёлых вычислений
  • устранение лишних промежуточных состояний

Применение с Motion One

Motion One опирается на Web Animations API и requestAnimationFrame для синхронизации анимаций. Однако внешние события, управляющие анимацией, часто требуют дополнительного контроля частоты.

Пример интеграции throttle с управлением прогрессом анимации:

import { animate } from "motion";

const controls = animate(0, 1, {
  duration: 1,
  onUpdate: (latest) => {
    console.log(latest);
  }
});

window.addEventListener(
  "scroll",
  rafThrottle(() => {
    const progress =
      window.scrollY / (document.body.scrollHeight - innerHeight);

    controls.currentTime = controls.duration * progress;
  })
);

В этом сценарии throttle через requestAnimationFrame предотвращает избыточные пересчёты currentTime, обеспечивая стабильную синхронизацию scroll → animation.

Debounce используется для пересоздания или переинициализации анимаций:

import { animate } from "motion";

let animation;

function initAnimation() {
  animation?.cancel();

  animation = animate(".box", {
    transform: ["translateY(0px)", "translateY(200px)"]
  });
}

window.addEventListener("resize", debounce(initAnimation, 200));

Pointer events и высокочастотные взаимодействия

События pointermove создают один из самых высоких потоков вызовов в браузере. Без throttle они быстро приводят к деградации производительности.

window.addEventListener(
  "pointermove",
  rafThrottle((e) => {
    const x = e.clientX;
    const y = e.clientY;

    document.documentElement.style.setProperty("--x", x);
    document.documentElement.style.setProperty("--y", y);
  })
);

При использовании Motion One такие координаты часто применяются для:

  • интерактивных параллакс-эффектов
  • следящих анимаций
  • динамических трансформаций элементов

Управление состоянием и накопление событий

Одной из ключевых проблем при отсутствии throttle/debounce является накопление устаревших вызовов. Это приводит к:

  • визуальному лагу между вводом и реакцией
  • неконсистентным состояниям анимации
  • перегрузке layout/reflow операций

Throttle решает это через ограничение частоты исполнения, debounce — через обнуление промежуточных вызовов.


Комбинированные стратегии

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

Пример: scroll влияет на анимацию (throttle), а изменение layout пересоздаёт её (debounce):

window.addEventListener(
  "scroll",
  rafThrottle(handleScroll)
);

window.addEventListener(
  "resize",
  debounce(() => {
    destroyAnimation();
    initAnimation();
  }, 300)
);

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

  • поток движения (scroll, pointermove) обрабатывается быстро и регулярно
  • поток структурных изменений (resize, DOM updates) обрабатывается редко и стабильно

Практика производительности в Motion One

При работе с анимациями критичны три фактора:

  • частота обновления состояния
  • синхронизация с кадрами браузера
  • минимизация layout thrashing

Throttle и debounce выступают инструментами контроля этих факторов, позволяя выстраивать предсказуемые и дешёвые по вычислениям анимационные системы, где Motion One отвечает за интерполяцию и рендер, а внешние утилиты — за поток событий.