Реалізація Intersection Observer для відстеження видимості елементів на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація Intersection Observer для відстеження видимості елементів на сайті
Проста
~1 робочий день
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Впровадження Intersection Observer для відстеження видимості елементів на веб-сайті

Intersection Observer — браузерний API для асинхронного спостереження за тим, входить ли елемент в область видимості viewport або іншого контейнера. Працює в окремому потоці, не блокує основний потік, не викликає layout thrashing — на відміну від старого підходу з getBoundingClientRect() в обробнику scroll.

Застосування: ледачое завантаження зображень, анімації при появленні, нескінченна прокрутка, відстеження прочитаного контенту, рекламні покази.

Базова настройка

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      // entry.isIntersecting — видно ли елемент
      // entry.intersectionRatio — частка видимої частини (0 до 1)
      // entry.boundingClientRect — розміри і положення елемента
      // entry.time — часова мітка
    })
  },
  {
    root: null,          // null = viewport
    rootMargin: '0px',   // відступи (як CSS margin)
    threshold: 0.1,      // спрацювати при 10% видимості
    // threshold: [0, 0.25, 0.5, 0.75, 1] — кілька порогів
  }
)

observer.observe(element)
observer.unobserve(element)
observer.disconnect() // зупинити всі спостереження

Анімації при появленні

Паттерн без лишних бібліотек:

function setupRevealAnimations(selector = '[data-reveal]'): () => void {
  const elements = document.querySelectorAll<HTMLElement>(selector)
  if (!elements.length) return () => {}

  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          const el = entry.target as HTMLElement
          el.classList.add('is-revealed')
          observer.unobserve(el)
        }
      })
    },
    { threshold: 0.15, rootMargin: '0px 0px -50px 0px' }
  )

  elements.forEach((el) => observer.observe(el))
  return () => observer.disconnect()
}

CSS:

[data-reveal] {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 500ms ease, transform 500ms ease;
}

[data-reveal].is-revealed {
  opacity: 1;
  transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  [data-reveal] { opacity: 1; transform: none; transition: none; }
}

Ледачое завантаження зображень

function lazyLoadImages(selector = 'img[data-src]'): void {
  const images = document.querySelectorAll<HTMLImageElement>(selector)

  // Нативне ледачое завантаження як основний метод
  if ('loading' in HTMLImageElement.prototype) {
    images.forEach((img) => {
      img.src = img.dataset.src!
      img.removeAttribute('data-src')
    })
    return
  }

  // Intersection Observer як fallback
  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (!entry.isIntersecting) return
        const img = entry.target as HTMLImageElement
        img.src = img.dataset.src!
        img.removeAttribute('data-src')
        observer.unobserve(img)
      })
    },
    { rootMargin: '200px 0px' } // завантажувати за 200px до появи
  )

  images.forEach((img) => observer.observe(img))
}

Нескінченна прокрутка

function createInfiniteScroll(
  sentinel: HTMLElement,
  onLoadMore: () => Promise<boolean> // повертає false, коли дані закінчилися
): () => void {
  let loading = false

  const observer = new IntersectionObserver(
    async (entries) => {
      const entry = entries[0]
      if (!entry.isIntersecting || loading) return

      loading = true
      const hasMore = await onLoadMore()
      loading = false

      if (!hasMore) observer.disconnect()
    },
    { rootMargin: '400px 0px' }
  )

  observer.observe(sentinel)
  return () => observer.disconnect()
}

Відстеження глибини прочитання (аналітика)

function trackReadDepth(
  article: HTMLElement,
  onMilestone: (percent: number) => void
): () => void {
  const thresholds = [0.25, 0.5, 0.75, 1.0]
  const reported = new Set<number>()

  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        const ratio = entry.intersectionRatio
        for (const t of thresholds) {
          if (ratio >= t && !reported.has(t)) {
            reported.add(t)
            onMilestone(t * 100)
          }
        }
      })
    },
    { threshold: thresholds }
  )

  observer.observe(article)
  return () => observer.disconnect()
}

// Використання:
trackReadDepth(articleEl, (percent) => {
  analytics.track('article_read_depth', { percent })
})

React Hook

function useIntersectionObserver(
  options: IntersectionObserverInit = {}
): [RefObject<HTMLElement | null>, boolean, IntersectionObserverEntry | null] {
  const ref = useRef<HTMLElement>(null)
  const [isVisible, setIsVisible] = useState(false)
  const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null)

  useEffect(() => {
    const el = ref.current
    if (!el) return

    const observer = new IntersectionObserver(
      ([entry]) => {
        setIsVisible(entry.isIntersecting)
        setEntry(entry)
      },
      options
    )

    observer.observe(el)
    return () => observer.disconnect()
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [options.threshold, options.rootMargin])

  return [ref, isVisible, entry]
}

// Використання:
function AnimatedCard() {
  const [ref, isVisible] = useIntersectionObserver({ threshold: 0.2 })

  return (
    <div
      ref={ref as React.RefObject<HTMLDivElement>}
      className={isVisible ? 'card card--visible' : 'card'}
    >
      ...
    </div>
  )
}

Що включено

Настройка спостерігачів для потрібних сценаріїв — анімацій, ледачого завантаження, нескінченного скролу, аналітики прочитання. React хуки, правильна очистка спостерігачів при розмонтуванні, prefers-reduced-motion для анімацій.

Терміни: 0,5 дня на базові сценарії, 1 день при кількох паттернах.