Debounce и throttle в JavaScript: укрощаем события

Содержание

О проверке кода. Примеры не запускались в браузере — в отличие от материалов по Node.js с фактическим выводом. Говорим прямо, а не ставим бейдж «проверено».

Некоторые события срабатывают лавиной: поле ввода — на каждый символ, прокрутка — десятки раз в секунду. Реагировать на каждое (запрос к серверу, тяжёлый пересчёт) — путь к тормозам. Debounce и throttle это лечат.

Проблема: лавина событий

// ❌ запрос на КАЖДЫЙ символ — при вводе "node" уйдёт 4 запроса подряд
input.addEventListener('input', (e) => {
  fetch(`/api/search?q=${e.target.value}`);
});

Пользователь печатает «node» — уходит четыре запроса, хотя нужен только последний. Прокрутка страницы вызывает обработчик десятки раз в секунду. Без ограничения это грузит и сеть, и процессор.

Debounce: дождаться паузы

Debounce откладывает выполнение, пока события не прекратятся на заданное время. Каждое новое событие сбрасывает таймер:

function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);                 // отменяем прошлый отложенный вызов
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// живой поиск: запрос только когда пользователь перестал печатать на 300мс
const search = debounce((query) => {
  fetch(`/api/search?q=${query}`);
}, 300);

input.addEventListener('input', (e) => search(e.target.value));

Пока символы идут подряд, clearTimeout сбрасывает таймер и запрос не уходит. Как только пользователь остановился на 300 мс — срабатывает один запрос с финальным значением. Внутри — замыкание хранит timer между вызовами, а setTimeout откладывает выполнение.

Throttle: не чаще раза в интервал

Throttle пропускает вызов, но не чаще одного раза за интервал — во время непрерывного события:

function throttle(fn, interval) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last >= interval) {        // прошёл ли интервал?
      last = now;
      fn.apply(this, args);
    }
  };
}

// прокрутка: реагируем максимум раз в 200мс, а не на каждое событие
const onScroll = throttle(() => {
  console.log('позиция:', window.scrollY);
}, 200);

window.addEventListener('scroll', onScroll);

Пока идёт прокрутка, функция выполняется регулярно — раз в 200 мс — а не только в конце. Это даёт равномерную реакцию во время процесса.

Разница наглядно

Debounce против throttle

Debounce — «конец серии»

Ждёт паузу

  • Выполняет ПОСЛЕ остановки событий
  • Промежуточные вызовы отменяются
  • Живой поиск, автосохранение
  • Валидация поля, реакция на ресайз

Throttle — «равномерно»

Ограничивает частоту

  • Выполняет ВО ВРЕМЯ серии
  • Не чаще раза в интервал
  • Прокрутка, движение мыши
  • Перетаскивание, отслеживание позиции

Простая аналогия. Debounce — как лифт, который ждёт, пока все зайдут, и только потом едет: реагирует на конец активности. Throttle — как турникет, пропускающий не чаще раза в секунду: реагирует равномерно во время потока. Выбор зависит от того, что важнее — финальный результат (debounce) или регулярная реакция (throttle).

Что выбрать по задаче

Задача Приём Почему
Живой поиск debounce нужен только финальный запрос
Автосохранение черновика debounce сохранить, когда перестали печатать
Валидация поля debounce проверить после ввода
Обработка прокрутки throttle реагировать регулярно во время скролла
Движение мыши, drag throttle равномерное обновление позиции
Кнопка «отправить» throttle не дать нажать дважды за момент

В реальных проектах

Понимать реализацию важно — она короткая и строится на замыканиях и таймерах. В боевом коде часто берут готовые _.debounce и _.throttle из lodash: они добавляют опции (выполнить сразу на первом событии, гарантировать финальный вызов, отмена). Но для простых случаев своя функция в несколько строк надёжна и не тянет зависимость. Частая связка — debounce на вводе плюс AbortController для отмены устаревших запросов.


Смежные темы: замыкания — как хранится таймер; таймеры — setTimeout внутри; fetch — живой поиск. Полный список — в уроках JavaScript.

Частые вопросы

В чём разница между debounce и throttle?
Debounce ждёт паузу: выполняет функцию только когда события прекратились на заданное время. Throttle ограничивает частоту: выполняет не чаще раза в интервал, пока события идут. Debounce для конца серии, throttle для равномерности.
Когда использовать debounce?
Когда важен только финальный результат серии событий: живой поиск (ждём, пока пользователь допечатает), автосохранение, валидация поля при вводе, реакция на изменение размера окна. Промежуточные срабатывания не нужны.
Когда использовать throttle?
Когда нужна регулярная реакция во время непрерывного события: отслеживание прокрутки, движения мыши, перетаскивания. Throttle гарантирует выполнение через равные интервалы, а не только в конце, как debounce.
Зачем вообще ограничивать события?
События ввода, прокрутки и ресайза срабатывают десятки раз в секунду. Если на каждое делать запрос к серверу или тяжёлый пересчёт, страница тормозит. Debounce и throttle снижают число вызовов без потери отзывчивости.
Нужно ли писать debounce самому?
Понимать реализацию полезно, она короткая. В реальных проектах часто берут готовую из lodash (_.debounce, _.throttle), но для простых случаев своя функция в несколько строк вполне надёжна.