Событие 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); // для страницы — правильно