Дебаунс та тротл у JavaScript
Що таке Debounce та Throttle?
Debounce та Throttle — це техніки оптимізації продуктивності, які контролюють частоту виконання функцій, особливо при обробці подій, які можуть викликатися дуже часто (прокрутка, зміна розміру, введення тощо).
Debounce
Debounce затримує виконання функції до тих пір, поки не пройде певний проміжок часу з моменту останнього виклику.
Коли використовувати?
- Пошук з автозаповненням (надсилати запит після того, як користувач перестане вводити)
- Валідація форм в реальному часі
- Збереження чернеток (автозбереження після паузи в редагуванні)
Приклад реалізації
function debounce(func, delay) {
let timeoutId;
return function(...args) {
// Очищення попереднього таймера
clearTimeout(timeoutId);
// Встановлення нового таймера
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Використання
const searchInput = document.querySelector('#search');
const handleSearch = debounce((event) => {
console.log('Search:', event.target.value);
// API запит
}, 500);
searchInput.addEventListener('input', handleSearch);Як це працює?
- При кожному виклику функції попередній таймер очищається
- Встановлюється новий таймер
- Функція виконується лише якщо з моменту останнього виклику пройшло
delayмілісекунд
Результат:
Якщо користувач вводить "javascript", замість 10 запитів (по одному на кожну літеру), буде надіслано лише один запит через 500 мс після завершення введення.
Throttle
Throttle забезпечує виконання функції не частіше ніж один раз за вказаний проміжок часу.
Коли використовувати?
- Обробка подій прокрутки (безкінечна прокрутка, ліниве завантаження)
- Обробка зміни розміру вікна
- Відстеження руху миші
- Кнопка відправлення (захист від множинних кліків)
Приклад реалізації
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// Використання
const handleScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
// Перевірка, чи досягнуто кінця сторінки
}, 1000);
window.addEventListener('scroll', handleScroll);Як це працює?
- При першому виклику функція виконується негайно
- Прапорець
inThrottleвстановлюється вtrue - Усі наступні виклики ігноруються, поки не пройде
limitмілісекунд - Після закінчення часу функцію можна викликати знову
Результат:
Якщо користувач швидко прокручує сторінку, функція буде викликатися максимум один раз на секунду, замість сотень разів на секунду.
Порівняння Debounce та Throttle
| Характеристика | Debounce | Throttle |
|---|---|---|
| Поведение | Виконується після паузи | Виконується з інтервалами |
| Перший виклик | Затримується | Виконується негайно |
| Часті виклики | Усі скидаються | Виконується один раз за N мс |
| Сфери використання | Пошук, автозбереження | Прокрутка, зміна розміру, відстеження |
Візуалізація
Уявіть, що подія відбувається 10 разів на секунду:
Без оптимізації:
█ █ █ █ █ █ █ █ █ █ (10 викликів)
З Debounce (500ms):
░ ░ ░ ░ ░ ░ ░ ░ ░ █ (1 виклик після паузи)
З Throttle (500ms):
█ ░ ░ ░ ░ █ ░ ░ ░ ░ (2 виклики з інтервалами)
Розширена реалізація з ведучими та слідуючими
Debounce з параметрами
function debounce(func, delay, { leading = false, trailing = true } = {}) {
let timeoutId;
return function(...args) {
const isInvokingLater = !timeoutId;
clearTimeout(timeoutId);
if (leading && isInvokingLater) {
func.apply(this, args);
}
timeoutId = setTimeout(() => {
if (trailing) {
func.apply(this, args);
}
timeoutId = null;
}, delay);
};
}leading: true— виклик на початкуtrailing: true— виклик в кінці (за замовчуванням)
Throttle з параметрами
function throttle(func, limit, { leading = true, trailing = true } = {}) {
let inThrottle;
let lastArgs;
return function(...args) {
if (!inThrottle) {
if (leading) {
func.apply(this, args);
}
inThrottle = true;
setTimeout(() => {
inThrottle = false;
if (trailing && lastArgs) {
func.apply(this, lastArgs);
lastArgs = null;
}
}, limit);
} else {
lastArgs = args;
}
};
}Використання бібліотек
У виробництві часто використовуються готові рішення:
Lodash
import { debounce, throttle } from 'lodash';
const debouncedSearch = debounce(searchFunction, 500);
const throttledScroll = throttle(scrollHandler, 1000);Скасування виконання
const debouncedFn = debounce(someFunction, 1000);
// Скасувати очікуючий виклик
debouncedFn.cancel();Приклад React
import { useCallback, useRef, useEffect } from 'react';
function useDebounce(callback, delay) {
const timeoutRef = useRef(null);
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return useCallback((...args) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
callback(...args);
}, delay);
}, [callback, delay]);
}
// Використання в компоненті
function SearchComponent() {
const handleSearch = useDebounce((value) => {
console.log('Searching:', value);
}, 500);
return (
<input
type="text"
onChange={(e) => handleSearch(e.target.value)}
/>
);
}Помилки та підводні камені
Забування про контекст this
// Неправильно
function debounce(func, delay) {
let timeoutId;
return function() {
clearTimeout(timeoutId);
timeoutId = setTimeout(func, delay); // втрата this
};
}
// Правильно
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay); // ✅
};
}Створення нової дебаунс-функції при кожному рендері
// Неправильно в React
function Component() {
// Кожен рендер створює нову функцію
const handler = debounce(() => {}, 500);
return <input onChange={handler} />;
}
// Правильно
function Component() {
// Функція створюється один раз
const handler = useMemo(
() => debounce(() => {}, 500),
[]
);
return <input onChange={handler} />;
}Висновок
- Debounce — затримує виконання до паузи в викликах (пошук, автозбереження)
- Throttle — обмежує частоту виконання (прокрутка, зміна розміру)
- Обидві техніки критично важливі для продуктивності додатків
- У виробництві використовуйте готові бібліотеки (lodash, underscore)
- Не забувайте про контекст (
this) та аргументи в реалізації - У React використовуйте useCallback або useMemo для збереження функцій між рендерами
На співбесідах:
Часто запитують реалізувати debounce/throttle з нуля та пояснити відмінності. Важливо розуміти не лише реалізацію, а й практичні випадки використання.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.