Debounce и throttle для событий

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


Debounce

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

Пример реализации debounce:

function debounce(func, delay) {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

Пояснение к коду:

  • timeoutId хранит идентификатор таймера.
  • clearTimeout сбрасывает предыдущий таймер при каждом новом вызове.
  • setTimeout выполняет функцию после указанной задержки, если не появится новый вызов.

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

window.addEventListener('resize', debounce(() => {
  console.log('Размер окна изменён');
}, 300));

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


Throttle

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

Пример реализации throttle:

function throttle(func, limit) {
  let lastFunc;
  let lastRan;
  return function(...args) {
    const context = this;
    if (!lastRan) {
      func.apply(context, args);
      lastRan = Date.now();
    } else {
      clearTimeout(lastFunc);
      lastFunc = setTimeout(function() {
        if ((Date.now() - lastRan) >= limit) {
          func.apply(context, args);
          lastRan = Date.now();
        }
      }, limit - (Date.now() - lastRan));
    }
  };
}

Пояснение к коду:

  • lastRan хранит время последнего вызова функции.
  • lastFunc используется для отложенного вызова, если событие произошло слишком часто.
  • Функция гарантирует, что вызовы происходят не чаще, чем limit миллисекунд.

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

window.addEventListener('scroll', throttle(() => {
  console.log('Прокрутка страницы');
}, 200));

Функция будет выполняться максимум один раз каждые 200 мс, даже если событие scroll срабатывает чаще.


Отличия debounce и throttle

Характеристика Debounce Throttle
Поведение при частых вызовах Выполняется только после окончания серии событий Выполняется регулярно, но не чаще заданного интервала
Идеально подходит для Поисковых форм, автосохранения, ресайза окна Прокрутки, анимаций, отслеживания позиции мыши
Основная цель Сокращение количества вызовов функции после активного события Ограничение частоты вызова функции

Применение в связке с GSAP

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

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

Пример: debounce для ресайза окна с GSAP

const updateLayout = () => {
  gsap.to('.box', { x: window.innerWidth / 2, duration: 0.5 });
};

window.addEventListener('resize', debounce(updateLayout, 250));

Пример: throttle для скролла с GSAP

const animateOnScroll = () => {
  gsap.to('.section', {
    y: window.scrollY / 2,
    duration: 0.3
  });
};

window.addEventListener('scroll', throttle(animateOnScroll, 100));

Продвинутые техники

  1. Immediate execution для debounce Можно вызвать функцию сразу при первом событии, а затем блокировать повторные вызовы на время задержки:
function debounceImmediate(func, delay, immediate = false) {
  let timeout;
  return function(...args) {
    const context = this;
    const callNow = immediate && !timeout;
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      timeout = null;
      if (!immediate) func.apply(context, args);
    }, delay);
    if (callNow) func.apply(context, args);
  };
}
  1. Throttle с leading и trailing вызовами Позволяет управлять, должен ли первый и/или последний вызов функции выполняться:
function throttleAdvanced(func, limit, options = { leading: true, trailing: true }) {
  let lastCall = 0;
  let timeout;
  return function(...args) {
    const now = Date.now();
    const context = this;
    const remaining = limit - (now - lastCall);

    if (remaining <= 0) {
      clearTimeout(timeout);
      lastCall = now;
      if (options.leading) func.apply(context, args);
    } else if (options.trailing) {
      clearTimeout(timeout);
      timeout = setTimeout(() => {
        lastCall = options.leading ? Date.now() : 0;
        func.apply(context, args);
      }, remaining);
    }
  };
}

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


Практические рекомендации

  • Использовать debounce для событий, где важно выполнить функцию только после завершения активности (например, ввод текста, изменение размеров окна).
  • Использовать throttle для событий, которые требуют регулярного обновления состояния интерфейса (например, скролл, движения мыши, анимации GSAP).
  • Всегда проверять производительность на мобильных устройствах, где частые вызовы функций могут сильно замедлять анимации.
  • Комбинировать с GSAP методы .kill(), .pause() и .resume() для управления анимациями при слишком частых событиях.

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