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 });
      // ...
    }
    

Связанные рецепты / темы