debounce для поиска
debounceоткладывает вызов функции до тех пор, пока пользователь не перестанет печатать — экономит API-запросы в поисковых полях.
Задача
При вводе в поле поиска не нужно запрашивать API на каждую нажатую клавишу. Нужно подождать паузу (300–500 мс) после последнего нажатия и только тогда сделать запрос.
Решение
Реализация debounce:
function debounce(fn, delay) {
let timerId;
return function (...args) {
clearTimeout(timerId);
timerId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
Применение к полю поиска:
const searchInput = document.getElementById('search');
const resultsContainer = document.getElementById('results');
async function fetchResults(query) {
if (!query.trim()) {
resultsContainer.innerHTML = '';
return;
}
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await res.json();
resultsContainer.innerHTML = data.items
.map((item) => `<li>${item.title}</li>`)
.join('');
}
const debouncedSearch = debounce(fetchResults, 400);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
TypeScript-версия с дженериками:
function debounce<T extends (...args: unknown) => void>(fn: T, delay: number): T {
let timerId: ReturnType<typeof setTimeout>;
return function (this: unknown, ...args: Parameters<T>) {
clearTimeout(timerId);
timerId = setTimeout( => fn.apply(this, args), delay);
} as T;
}
Ключевые моменты
clearTimeoutперед каждымsetTimeout— суть debounce: таймер сбрасывается при каждом новом вызове.- Задержка 300–500 мс — оптимум для поиска; меньше — слишком частые запросы, больше — заметная задержка.
- Не забудь
encodeURIComponentпри подстановке пользовательского ввода в URL. debounceвозвращает новую функцию — объявляйdebouncedSearchодин раз вне обработчика событий.
Варианты
- throttle — другой паттерн: ограничивает частоту вызовов (раз в N мс), а не ждёт паузы. Подходит для scroll/resize. См. Рецепт -- throttle для скролла.
- Lodash —
_.debounce(fn, 300, { leading: false, trailing: true })с опциямиleading/trailing. - AbortController — при частых запросах отменяй предыдущий запрос, а не только откладывай:
let controller; async function search(query) { controller?.abort(); controller = new AbortController(); const res = await fetch(`/api/search?q=${query}`, { signal: controller.signal }); // ... }