Hack Frontend Community

Debounce и Throttle в JavaScript

Что такое Debounce и Throttle?

Debounce и Throttle — это техники оптимизации производительности, которые контролируют частоту выполнения функций, особенно при обработке событий, которые могут вызываться очень часто (scroll, resize, input и т.д.).


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('Поиск:', event.target.value);
  // API запрос
}, 500);

searchInput.addEventListener('input', handleSearch);

Как это работает?

  1. При каждом вызове функции сбрасывается предыдущий таймер
  2. Устанавливается новый таймер
  3. Функция выполняется только если после последнего вызова прошло delay миллисекунд

Результат:

Если пользователь печатает "javascript", вместо 10 запросов (по одному на каждую букву) отправится всего один запрос через 500мс после того, как пользователь закончит печатать.


Throttle (Троттлинг)

Throttle гарантирует, что функция будет выполняться не чаще, чем раз в указанный интервал времени.

Когда использовать?

  • Обработка события scroll (бесконечная прокрутка, lazy loading)
  • Обработка resize окна
  • Отслеживание движения мыши
  • Кнопка отправки формы (защита от множественных кликов)

Пример реализации

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);

Как это работает?

  1. При первом вызове функция выполняется сразу
  2. Устанавливается флаг inThrottle = true
  3. Все последующие вызовы игнорируются, пока не истечёт limit миллисекунд
  4. После истечения времени функция снова может быть вызвана

Результат:

Если пользователь быстро скроллит страницу, функция будет вызываться максимум раз в секунду, вместо сотен раз в секунду.


Сравнение Debounce и Throttle

ХарактеристикаDebounceThrottle
ПоведениеВыполняется после паузыВыполняется через интервалы
Первый вызовОткладываетсяВыполняется сразу
Частые вызовыВсе сбрасываютсяВыполняется раз в N мс
ИспользованиеПоиск, автосохранениеScroll, resize, tracking

Визуализация

Представим, что событие происходит 10 раз за секунду:

Без оптимизации:

█ █ █ █ █ █ █ █ █ █  (10 вызовов)

С Debounce (500ms):

░ ░ ░ ░ ░ ░ ░ ░ ░ █  (1 вызов после паузы)

С Throttle (500ms):

█ ░ ░ ░ ░ █ ░ ░ ░ ░  (2 вызова с интервалами)

Продвинутая реализация с leading и trailing

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

Создавать новую debounced функцию при каждом рендере

// Неправильно в React
function Component() {
  // Каждый рендер создаёт новую функцию
  const handler = debounce(() => {}, 500);
  
  return <input onChange={handler} />;
}

// Правильно
function Component() {
  // Функция создаётся один раз
  const handler = useMemo(
    () => debounce(() => {}, 500),
    []
  );
  
  return <input onChange={handler} />;
}

Вывод

  • Debounce — откладывает выполнение до паузы в вызовах (поиск, автосохранение)
  • Throttle — ограничивает частоту выполнения (scroll, resize)
  • Обе техники критичны для производительности приложений
  • В продакшене используйте готовые библиотеки (lodash, underscore)
  • Не забывайте про контекст (this) и аргументы при реализации
  • В React используйте useCallback или useMemo для сохранения функций между рендерами

На собеседовании:

Часто просят реализовать debounce/throttle с нуля и объяснить различия. Важно понимать не только реализацию, но и практические кейсы применения.