Що таке debounce?
Debounce - це техніка, яка затримує виклик функції до моменту, коли користувач зупинив дії, і тоді виконує її один раз.
Теорія
TL;DR
- Debounce чекає на тишу. Кожен новий виклик скидає таймер і починає відлік знову.
- Аналогія: пошукове поле, яке чекає 300мс після останнього натиснення клавіші і тільки тоді робить запит до API.
- Головна різниця з throttle: debounce спрацьовує після того як активність зупинилась; throttle спрацьовує під час активності з фіксованим інтервалом.
- Використовуй коли важливе фінальне значення, а не проміжні.
- Правило вибору: користувач швидко друкує і потрібен один API-запит? Debounce. Scroll потребує плавних оновлень? Throttle.
Швидкий приклад
function debounce(func, delay) {
let timer;
return function(...args) {
clearTimeout(timer); // Скасовуємо попередній таймер
timer = setTimeout(() => func.apply(this, args), delay);
};
}
const searchAPI = debounce((query) => console.log('API call:', query), 300);
searchAPI('h'); // Таймер скинуто
searchAPI('he'); // Таймер скинуто
searchAPI('hello'); // 300мс тиші...
// Результат: "API call: hello"Проходить тільки останній виклик. Решта ігноруються.
Як це працює
Кожен виклик debounce-функції запускає clearTimeout для попереднього таймера і ставить новий. Функція виконується тільки коли повна затримка проходить без нових викликів. JavaScript обробляє це у фазі таймерів event loop, тому CPU не витрачається даремно. Просто відкладена задача чекає у черзі.
Коли використовувати
- Пошуковий рядок або автодоповнення: запит до API один раз після паузи, а не на кожне натиснення клавіші.
- Зміна розміру вікна (resize): перерахунок макету після того як користувач зупинив перетягування, а не 60 разів на секунду.
- Валідація форми: показуй помилку після того як користувач закінчив редагувати, а не посередині слова.
- Не використовуй debounce для scroll якщо потрібні плавні візуальні оновлення. Там краще throttle або
requestAnimationFrame.
Типові помилки
Немає очищення при unmount у React:
На практиці це найпоширеніша debounce-помилка в React-кодових базах.
// Неправильно
useEffect(() => {
const db = debounce(handleSearch, 300);
// не повертаємо cleanup
}, []);
// Правильно
useEffect(() => {
const db = debounce(handleSearch, 300);
return () => clearTimeout(db.timer);
}, []);Без cleanup таймер спрацює після того як компонент зникне з DOM. React покаже попередження: "Can't perform a state update on an unmounted component."
Однаковий delay для всіх подій:
const dbResize = debounce(handleResize, 1000); // Занадто повільноОбробники resize потребують 16-50мс щоб відчуватись responsive при 60fps. 300мс підходить для введення тексту. 1000мс помітно повільно для будь-чого візуального.
Створення нового debouncer всередині функції рендеру:
// Неправильно - новий debouncer на кожен рендер, таймер завжди скидається
function Search() {
const debouncedSearch = debounce(fetchResults, 300);
}
// Правильно - стабільне посилання між рендерами
const debouncedSearch = useCallback(debounce(fetchResults, 300), []);Де використовується у продакшені
- React:
lodash.debounceуuseCallbackдля пошукових полів. - Next.js: debounce для динамічних пошукових сторінок при зміні маршруту.
- VS Code: debounce обробників команд у розширеннях.
- Node/Express:
p-debounceдля rate-limiting важких запитів.
Питання на співбесіді
Q: Реалізуй debounce з нуля.
A: Використовуй setTimeout і clearTimeout. Зберігай ID таймера у замиканні (closure). При кожному виклику скасовуй старий таймер і ставь новий. Згадай обробку this і args через .apply().
Q: Debounce проти throttle?
A: Debounce спрацьовує один раз після того як активність зупиниться. Throttle спрацьовує максимум один раз за інтервал, навіть під час постійної активності. Throttle підходить для scroll або mousemove де потрібні регулярні оновлення, а не тільки фінальне значення.
Q: Як використовувати debounce у React хуку?
A: Обгорни у useCallback(debounce(fn, delay), []) щоб зберегти стабільне посилання між рендерами. Очищай через clearTimeout у return функції useEffect.
Q: Що відбувається з debounce у React StrictMode?
A: StrictMode перемонтує компоненти у development режимі. Без стабільного ref з'являються "сирітські" таймери, що спрацьовують після unmount. Рішення: зберігай debouncer у useRef і скасовуй при unmount. Для concurrent React useDeferredValue часто є зручнішою альтернативою ручному debounce на полях введення.
Приклади
Базовий: пошукове поле без фреймворку
function debounce(func, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
const input = document.getElementById('search');
const handleInput = debounce((e) => {
console.log('Пошук:', e.target.value);
}, 300);
input.addEventListener('input', handleInput);
// Швидке введення "react": один лог після паузи, а не п'ятьОдин event listener, один API-запит на паузу. Без debounce кожне натиснення клавіші запускає fetch.
Середній рівень: React компонент пошуку
import { useState, useCallback } from 'react';
function debounce(func, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
function SearchComponent() {
const [results, setResults] = useState([]);
const debouncedSearch = useCallback(
debounce((query) => {
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(setResults);
}, 300),
[]
);
return (
<input
onChange={(e) => debouncedSearch(e.target.value)}
placeholder="Search..."
/>
);
}
// Швидке введення "react hooks": один API-запит після паузиuseCallback з порожнім масивом залежностей зберігає стабільне посилання на debouncer між рендерами. Якщо створювати його заново на кожен рендер, таймер скидається на кожне натиснення і debounce не спрацює.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.