← Назад к вопросам

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

1.8 Middle🔥 212 комментариев
#JavaScript Core#Оптимизация и производительность

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Throttle: когда и зачем использовать

Throttle (дросселирование) — это техника оптимизации производительности, которая ограничивает частоту вызова функции. Функция вызывается максимум один раз за определённый промежуток времени, независимо от того, сколько раз произошло событие.

Проблема, которую решает throttle

Представьте, что пользователь скролит страницу:

// Без throttle: event срабатывает может быть 100+ раз в секунду
window.addEventListener('scroll', () => {
  updateScrollPosition(); // Вызывается 100+ раз!
  saveToDatabase();       // Отправляет 100+ запросов
  updateAnalytics();      // 100+ запросов на аналитику
});

Это приводит к:

  • Перегрузке CPU
  • Лагам и зависанию интерфейса
  • Огромному количеству ненужных запросов к серверу
  • Истощению батареи на мобильных устройствах

Throttle решает это, гарантируя, что функция вызывается максимум один раз в N миллисекунд.

Throttle vs Debounce

Это важное различие, которое спрашивают на интервью:

ПараметрThrottleDebounce
ВызовРегулярно (каждые N мс)Только после паузы
СрабатываниеВо время событияПосле завершения события
ИспользованиеScroll, resize, mousemoveПоиск, resize окна, фильтр
ГарантияФункция вызывается N раз за T времениФункция вызывается 1 раз после T времени

Пример:

  • Throttle при скролле: каждые 100мс отправляем аналитику, пока пользователь скролит
  • Debounce при поиске: отправляем поиск только после того, как пользователь 300мс не печатал

Реализация 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 event');
}, 1000); // Максимум один раз в секунду

window.addEventListener('scroll', handleScroll);

Продвинутая версия (с leading и trailing)

function throttle(func, limit, options = {}) {
  let inThrottle, lastRan;
  const { leading = true, trailing = true } = options;
  
  return function(...args) {
    const now = Date.now();
    
    // Вызываем функцию в начале периода (leading)
    if (!lastRan && leading) {
      func.apply(this, args);
      lastRan = now;
    } else {
      // Очищаем предыдущий timeout
      clearTimeout(inThrottle);
      
      // Вызываем функцию в конце периода (trailing)
      if (trailing) {
        inThrottle = setTimeout(() => {
          if (Date.now() - lastRan >= limit) {
            func.apply(this, args);
            lastRan = Date.now();
          }
        }, limit - (now - lastRan));
      }
    }
  };
}

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

1. Scroll event (самый частый случай)

const handleScroll = throttle(() => {
  const scrollTop = window.scrollY;
  console.log('Текущая позиция:', scrollTop);
  
  // Отправляем аналитику
  trackScrollEvent(scrollTop);
  
  // Загружаем больше контента
  if (isNearBottom()) {
    loadMoreContent();
  }
}, 300); // Каждые 300мс

window.addEventListener('scroll', handleScroll);

2. Resize event

const handleResize = throttle(() => {
  const width = window.innerWidth;
  const height = window.innerHeight;
  
  // Пересчитываем layout
  recalculateLayout(width, height);
}, 500);

window.addEventListener('resize', handleResize);

3. Mouse move (отслеживание курсора)

const handleMouseMove = throttle((event) => {
  const { clientX, clientY } = event;
  
  // Отправляем координаты на сервер (для многопользовательских приложений)
  updateCursorPosition(clientX, clientY);
}, 100); // Обновляем позицию курсора каждые 100мс

document.addEventListener('mousemove', handleMouseMove);

4. Отправка API запросов (например, при вводе параметров фильтра)

const handleFilterChange = throttle((filterValue) => {
  // Отправляем фильтр максимум раз в 500мс
  fetchFilteredResults(filterValue);
}, 500);

input.addEventListener('input', (e) => {
  handleFilterChange(e.target.value);
});

Обратите внимание: этот случай близок к debounce, но throttle гарантирует, что мы отправляем запрос регулярно, а не только в конце.

5. Обновление фиксированного навигационного элемента при скролле

const updateNavBar = throttle(() => {
  const scrollY = window.scrollY;
  const navBar = document.querySelector('.navbar');
  
  if (scrollY > 100) {
    navBar.classList.add('scrolled');
  } else {
    navBar.classList.remove('scrolled');
  }
}, 100);

window.addEventListener('scroll', updateNavBar);

Реальный пример: infinite scroll с throttle

class InfiniteScroll {
  constructor() {
    this.isLoading = false;
    this.page = 1;
    
    // Throttle функцию проверки
    this.checkScroll = throttle(() => {
      this.loadMoreIfNeeded();
    }, 300);
    
    window.addEventListener('scroll', this.checkScroll);
  }
  
  loadMoreIfNeeded() {
    const scrollTop = window.scrollY;
    const docHeight = document.documentElement.scrollHeight;
    const windowHeight = window.innerHeight;
    
    // Если пользователь прокрутил до конца
    if (scrollTop + windowHeight > docHeight - 500 && !this.isLoading) {
      this.loadMore();
    }
  }
  
  async loadMore() {
    this.isLoading = true;
    this.page++;
    
    try {
      const response = await fetch(`/api/items?page=${this.page}`);
      const data = await response.json();
      this.renderItems(data);
    } finally {
      this.isLoading = false;
    }
  }
}

Практические рекомендации

Выбор промежутка времени

  • 100мс — для очень частых событий (mousemove, wheel)
  • 300мс — для scroll, resize
  • 500мс — для API запросов, фильтров
  • 1000мс — для аналитики, логирования

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

  • Клики по кнопкам — используй обычный click handler с флагом isLoading
  • Форм-события — используй debounce вместо throttle
  • Единовременные события — нет смысла в throttle

Альтернативы

requestAnimationFrame (рекомендуется для визуальных обновлений)

let ticking = false;

function updateScroll() {
  // Обновляем визуальное состояние
  renderUI();
  ticking = false;
}

window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(updateScroll);
    ticking = true;
  }
});

Это лучше, чем setTimeout, потому что синхронизирует с частотой обновления экрана (60 FPS).

Использование lodash

В реальных проектах часто используют готовые решения:

import { throttle } from 'lodash';

const handleScroll = throttle(() => {
  updateUI();
}, 300);

window.addEventListener('scroll', handleScroll);

Типичные ошибки

// Ошибка 1: Создание новой функции throttle каждый раз
window.addEventListener('scroll', () => {
  throttle(() => updateUI(), 300)(); // Неправильно!
});

// Правильно:
const handleScroll = throttle(() => updateUI(), 300);
window.addEventListener('scroll', handleScroll);

// Ошибка 2: Забыли bind
const obj = {
  count: 0,
  increment: throttle(function() {
    this.count++; // this может быть undefined!
  }, 100)
};

Ключевые выводы

  1. Throttle = максимум одного вызова в N мс
  2. Используй для высокочастотных событий: scroll, resize, mousemove
  3. Debounce для событий, где важен финальный результат: поиск, валидация
  4. requestAnimationFrame лучше для визуальных обновлений
  5. Не забывай о производительности: неправильная throttle ухудшит UX