Throttle для скролла

throttle ограничивает частоту вызова функции — не чаще одного раза в N миллисекунд — идеально для событий scroll и resize.

Задача

Событие scroll может срабатывать десятки раз в секунду. Тяжёлый обработчик (вычисления позиций, DOM-манипуляции) должен вызываться не чаще, чем нужно — раз в 16 мс (60 fps) или реже.

Решение

Реализация throttle:

function throttle(fn, limit) {
  let lastCall = 0;

  return function (...args) {
    const now = Date.now();
    if (now - lastCall >= limit) {
      lastCall = now;
      fn.apply(this, args);
    }
  };
}

Применение к событию scroll:

function onScroll() {
  const scrollY = window.scrollY;

  // Показать кнопку "наверх" при прокрутке > 300px
  const btn = document.getElementById('backToTop');
  btn.hidden = scrollY < 300;

  // Показать прогресс-бар чтения
  const progress = document.getElementById('readProgress');
  const docHeight = document.documentElement.scrollHeight - window.innerHeight;
  progress.style.width = `${(scrollY / docHeight) * 100}%`;
}

window.addEventListener('scroll', throttle(onScroll, 100), { passive: true });

requestAnimationFrame-вариант — для анимационной синхронизации:

function throttleRAF(fn) {
  let rafId = null;

  return function (...args) {
    if (rafId) return;
    rafId = requestAnimationFrame(() => {
      fn.apply(this, args);
      rafId = null;
    });
  };
}

window.addEventListener('scroll', throttleRAF(onScroll), { passive: true });

Ключевые моменты

  • { passive: true } в addEventListener — браузер знает, что обработчик не вызовет preventDefault, и не блокирует скролл; критично для производительности.
  • throttle vs debounce: throttle вызывает регулярно (раз в N мс), debounce ждёт паузы. Для scroll — throttle; для input поиска — debounce.
  • requestAnimationFrame-вариант привязывает вызов к кадрам рендера (~16 мс), что оптимально для визуальных изменений.
  • Date.now() точнее new Date и не создаёт объект — предпочтительно в горячем пути.

Варианты

  • IntersectionObserver — для отслеживания видимости элементов при скролле без scroll события вообще (более эффективно).
  • Lodash _.throttle(fn, 100) — с опциями leading/trailing для точного управления.

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