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

Как посчитать когда пользователь находится на нужном моменте при 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 для надежности
Как посчитать когда пользователь находится на нужном моменте при scroll? | PrepBro