← Назад к вопросам
Как посчитать когда пользователь находится на нужном моменте при scroll?
2.0 Middle🔥 142 комментариев
#JavaScript Core
Комментарии (2)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Детектирование позиции пользователя при scroll
Выявление того, когда пользователь достигает определенного места на странице при прокрутке, необходимо для реализации множества функций: бесконечная загрузка, аналитика, sticky элементы, lazy loading. Рассмотрим несколько методов от базовых до современных.
1. Scroll Event - базовый подход
let isScrolling = false;
window.addEventListener('scroll', () => {
const scrollTop = window.scrollY || document.documentElement.scrollTop;
const targetPosition = 1000; // Позиция в px
if (scrollTop >= targetPosition && !isScrolling) {
isScrolling = true;
console.log('User reached target position');
// Выполнить действие
} else if (scrollTop < targetPosition) {
isScrolling = false;
}
});
// Или для определенного элемента
window.addEventListener('scroll', () => {
const section = document.getElementById('target-section');
const rect = section.getBoundingClientRect();
if (rect.top <= window.innerHeight && rect.bottom >= 0) {
console.log('Section is visible in viewport');
}
});
Проблемы scroll события:
- Вызывается много раз в секунду (не оптимально)
- Может привести к jank если обработчик тяжелый
2. Throttling для оптимизации
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
const handleScroll = throttle(() => {
const scrollTop = window.scrollY;
console.log('Scroll position:', scrollTop);
// Проверка позиции
}, 200); // Максимум раз в 200ms
window.addEventListener('scroll', handleScroll);
3. RequestAnimationFrame для гладкого доступа
let ticking = false;
let scrollPosition = 0;
window.addEventListener('scroll', () => {
scrollPosition = window.scrollY;
if (!ticking) {
window.requestAnimationFrame(() => {
// Обновление происходит синхронно с браузером
checkScrollPosition(scrollPosition);
ticking = false;
});
ticking = true;
}
});
function checkScrollPosition(y) {
const target = document.getElementById('target');
const rect = target.getBoundingClientRect();
if (rect.top <= window.innerHeight && rect.bottom >= 0) {
console.log('Target is visible');
}
}
4. Intersection Observer - современный подход
Это наиболее эффективный и правильный способ:
const observerOptions = {
root: null, // viewport
rootMargin: '0px', // отступ от края viewport
threshold: 0.5 // 50% элемента видимо
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('Element is visible:', entry.target.id);
entry.target.classList.add('visible');
// Опционально: остановить наблюдение
// observer.unobserve(entry.target);
} else {
entry.target.classList.remove('visible');
}
});
}, observerOptions);
// Наблюдение за одним элементом
const target = document.getElementById('lazy-section');
observer.observe(target);
// Наблюдение за несколькими
document.querySelectorAll('.trackable').forEach(el => {
observer.observe(el);
});
5. IntersectionObserver для бесконечной загрузки
const loadMoreButton = document.getElementById('load-more');
const infiniteScrollObserver = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
console.log('Reached end of page, loading more...');
loadMoreItems();
}
},
{ threshold: 0.1 }
);
infiniteScrollObserver.observe(loadMoreButton);
async function loadMoreItems() {
try {
const response = await fetch('/api/items?page=2');
const data = await response.json();
// Добавить новые элементы
appendItems(data);
} catch (error) {
console.error('Failed to load items:', error);
}
}
6. React хук для Intersection Observer
import { useEffect, useRef, useState } from 'react';
function useIntersection(options = {}) {
const elementRef = useRef(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
// Опционально: остановить после первого обнаружения
// observer.unobserve(entry.target);
} else {
setIsVisible(false);
}
}, options);
if (elementRef.current) {
observer.observe(elementRef.current);
}
return () => {
if (elementRef.current) {
observer.unobserve(elementRef.current);
}
};
}, [options]);
return [elementRef, isVisible];
}
// Использование
function Component() {
const [ref, isVisible] = useIntersection({ threshold: 0.5 });
return (
<section ref={ref}>
{isVisible && <p>Section is visible!</p>}
</section>
);
}
7. Отслеживание процента прокрутки страницы
function getScrollPercentage() {
const windowHeight = document.documentElement.scrollHeight - window.innerHeight;
const scrolled = window.scrollY;
return windowHeight > 0 ? (scrolled / windowHeight) * 100 : 0;
}
window.addEventListener('scroll', throttle(() => {
const percentage = getScrollPercentage();
console.log(`Scrolled ${percentage.toFixed(2)}%`);
// Отправить аналитику на 50% и 90%
if (percentage >= 50) {
trackEvent('scroll-50-percent');
}
if (percentage >= 90) {
trackEvent('scroll-near-bottom');
}
}, 500));
8. Lazy Loading изображений
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.add('loaded');
imageObserver.unobserve(img);
}
});
}, {
rootMargin: '100px' // Начать загрузку за 100px до видимости
});
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});
// HTML
// <img data-src="image.jpg" src="placeholder.jpg" />
9. Sticky header с отслеживанием
const header = document.querySelector('header');
const firstSection = document.querySelector('section');
const stickyObserver = new IntersectionObserver(([entry]) => {
if (!entry.isIntersecting) {
header.classList.add('sticky-active');
} else {
header.classList.remove('sticky-active');
}
}, {
threshold: 0
});
stickyObserver.observe(firstSection);
10. Отслеживание активного раздела при навигации
function useActiveSection() {
const [activeSection, setActiveSection] = useState(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setActiveSection(entry.target.id);
}
});
},
{ threshold: 0.5 }
);
document.querySelectorAll('section').forEach(section => {
observer.observe(section);
});
return () => observer.disconnect();
}, []);
return activeSection;
}
function Navigation() {
const activeSection = useActiveSection();
return (
<nav>
{['intro', 'features', 'pricing'].map(section => (
<a
key={section}
href={`#${section}`}
className={activeSection === section ? 'active' : ''}
>
{section}
</a>
))}
</nav>
);
}
Сравнение методов
| Метод | Производительность | Браузеры | Сложность |
|---|---|---|---|
| Scroll event | Низкая | Все | Низкая |
| Throttle | Средняя | Все | Средняя |
| RAF | Хорошая | Все | Средняя |
| Intersection Observer | Отличная | Современные | Низкая |
Ключевые моменты
- Intersection Observer - лучший выбор для большинства случаев
- Использует native browser API - оптимально по производительности
- Поддерживает multiple элементы и rootMargin для гибкости
- ScrollEvent + throttle/RAF все еще применим для простых случаев
- Для аналитики используй sendBeacon для надежности