Событие scroll

Событие scroll генерируется при прокрутке элемента или страницы; оно не всплывает (кроме document), срабатывает очень часто и требует оптимизации через throttle или requestAnimationFrame.

Зачем нужно

Событие scroll — основа для таких UI-паттернов как sticky-навигация, бесконечная прокрутка (infinite scroll), анимация при входе в поле зрения, индикаторы прогресса чтения и lazy loading изображений. Без грамотной оптимизации обработчик scroll может вызывать layout thrashing и замедлять страницу.

Где используется

  • Sticky-хедер: фиксируется после прокрутки
  • Infinite scroll: подгрузка данных при приближении к низу
  • Анимация появления элементов (scroll reveal)
  • Прогресс-бар статьи
  • Параллакс-эффекты
  • Lazy loading изображений

Базовое использование

// Прокрутка страницы
window.addEventListener('scroll', () => {
  const scrollTop = window.scrollY; // или document.documentElement.scrollTop
  console.log('Прокрутка:', scrollTop, 'px');
});

// Прокрутка конкретного элемента
const container = document.querySelector('.scrollable');
container.addEventListener('scroll', () => {
  console.log('scrollTop:', container.scrollTop);
  console.log('scrollLeft:', container.scrollLeft);
});

Полезные свойства при работе со scroll

// Позиция прокрутки
window.scrollX;     // горизонтальная
window.scrollY;     // вертикальная (современный способ)
pageXOffset;        // псевдоним scrollX (устаревший)
pageYOffset;        // псевдоним scrollY

// Размеры документа и viewport
document.documentElement.scrollHeight; // полная высота страницы
document.documentElement.clientHeight; // высота видимой области
document.body.scrollHeight;             // высота тела

// Достигнут ли конец страницы?
function isAtBottom() {
  const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
  return scrollTop + clientHeight >= scrollHeight - 5; // погрешность 5px
}

Sticky-хедер

const header = document.querySelector('.header');
const stickyThreshold = 100; // px

window.addEventListener('scroll', () => {
  if (window.scrollY > stickyThreshold) {
    header.classList.add('sticky');
  } else {
    header.classList.remove('sticky');
  }
});

Оптимизация: throttle

Событие scroll может срабатывать 60+ раз в секунду. Throttle ограничивает частоту вызовов:

function throttle(fn, delay) {
  let last = 0;
  return function(...args) {
    const now = Date.now();
    if (now - last >= delay) {
      last = now;
      fn.apply(this, args);
    }
  };
}

const handleScroll = throttle(() => {
  updateProgressBar;
}, 100); // не чаще раза в 100ms

window.addEventListener('scroll', handleScroll);

Оптимизация: requestAnimationFrame

let ticking = false;

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

function updateHeader(scrollY) {
  header.style.opacity = Math.max(0, 1 - scrollY / 300);
}

Intersection Observer (современная альтернатива)

Для lazy loading и reveal-анимаций лучше использовать IntersectionObserver — не вешает обработчик scroll:

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add('visible');
      observer.unobserve(entry.target); // перестаём следить
    }
  });
}, {
  threshold: 0.1 // 10% элемента видно
});

document.querySelectorAll('.animate-on-scroll').forEach(el => {
  observer.observe(el);
});

Программная прокрутка

// Мгновенно
window.scrollTo(0, 500);
window.scrollTo({ top: 500, behavior: 'instant' });

// Плавно
window.scrollTo({ top: 500, behavior: 'smooth' });
window.scrollBy({ top: 200, behavior: 'smooth' }); // относительно текущей позиции

// К элементу
document.querySelector('#section2').scrollIntoView({ behavior: 'smooth' });

Частые ошибки

1. Тяжёлые вычисления в обработчике без throttle

// Плохо: вызывается 60+ раз/сек, getBoundingClientRect вызывает reflow
window.addEventListener('scroll', () => {
  const rect = heavyElement.getBoundingClientRect(); // reflow!
  if (rect.top < 0) doSomething;
});

2. scroll не всплывает (bubbles: false)

// Обработчик на document не поймает scroll с div
document.addEventListener('scroll', handler);      // не сработает для div!
divElement.addEventListener('scroll', handler);    // правильно — на самом элементе
window.addEventListener('scroll', handler);        // для страницы — правильно

Связанные темы

Ресурсы