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 | нужен только финальный запрос |
| Автосохранение черновика | debounce | сохранить, когда перестали печатать |
| Валидация поля | debounce | проверить после ввода |
| Обработка прокрутки | throttle | реагировать регулярно во время скролла |
| Движение мыши, drag | throttle | равномерное обновление позиции |
| Кнопка «отправить» | throttle | не дать нажать дважды за момент |
В реальных проектах
Понимать реализацию важно — она короткая и строится на замыканиях и таймерах. В боевом коде часто берут готовые _.debounce и _.throttle из lodash: они добавляют опции (выполнить сразу на первом событии, гарантировать финальный вызов, отмена). Но для простых случаев своя функция в несколько строк надёжна и не тянет зависимость. Частая связка — debounce на вводе плюс AbortController для отмены устаревших запросов.
Смежные темы: замыкания — как хранится таймер; таймеры — setTimeout внутри; fetch — живой поиск. Полный список — в уроках JavaScript.