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

Что такое Intersection Observer?

1.7 Middle🔥 221 комментариев
#Браузер и сетевые технологии#Оптимизация и производительность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Intersection Observer API: Мониторинг видимости элементов

Intersection Observer — это браузерный API, который позволяет асинхронно отслеживать, когда элемент становится видимым или невидимым в viewport браузера. Это мощный инструмент для оптимизации производительности и создания интерактивного контента.

Что такое Intersection Observer

Intersection Observer наблюдает за пересечением (intersection) элемента с видимой областью экрана или с другим элементом. Когда элемент входит или выходит из видимой области, срабатывает callback функция.

Основные применения:

  • Ленивая загрузка (lazy loading) — загружай изображения только когда они видны
  • Бесконечный скролл — загружай больше контента при скролле вниз
  • Аналитика — отслеживай, какие элементы пользователь видел
  • Вход в область экрана (animations) — запусти анимацию когда элемент видно

Синтаксис и примеры

Базовый пример: Ленивая загрузка изображений

export function LazyImage() {
  const imageRef = useRef<HTMLImageElement>(null);
  const [imageSrc, setImageSrc] = useState('');

  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        // Когда элемент видно
        if (entry.isIntersecting) {
          const img = entry.target as HTMLImageElement;
          // Загружаем полное изображение
          setImageSrc(img.dataset.src || '');
          // Перестаём наблюдать, так как изображение уже загружено
          observer.unobserve(img);
        }
      });
    });

    if (imageRef.current) {
      observer.observe(imageRef.current);
    }

    return () => {
      if (imageRef.current) {
        observer.unobserve(imageRef.current);
      }
    };
  }, []);

  return (
    <img
      ref={imageRef}
      src={imageSrc}
      data-src="https://example.com/image.jpg"
      alt="Lazy loaded image"
      className="w-full h-auto"
    />
  );
}

Бесконечный скролл (infinite scroll)

export function InfiniteScrollList() {
  const [items, setItems] = useState<string[]>([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const sentinelRef = useRef<HTMLDivElement>(null);

  const loadMore = async () => {
    setLoading(true);
    const newItems = await fetchItems(page);
    setItems((prev) => [...prev, ...newItems]);
    setPage((prev) => prev + 1);
    setLoading(false);
  };

  useEffect(() => {
    // Когда sentinel элемент видно, загружаем ещё
    const observer = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting && !loading) {
        loadMore();
      }
    });

    if (sentinelRef.current) {
      observer.observe(sentinelRef.current);
    }

    return () => {
      if (sentinelRef.current) {
        observer.unobserve(sentinelRef.current);
      }
    };
  }, [loading, page]);

  return (
    <div>
      {items.map((item, i) => (
        <div key={i} className="p-4 border-b border-border-1">
          {item}
        </div>
      ))}

      {/* Sentinel элемент — когда он видно, загружаем ещё */}
      <div ref={sentinelRef} className="p-4 text-center">
        {loading && 'Loading...'}
      </div>
    </div>
  );
}

Конфигурация Intersection Observer

Можешь настроить поведение через options:

const options = {
  // root: null означает viewport (видимая область)
  root: null,

  // Отступ вокруг root'а
  // Негативное значение = срабатывает раньше
  rootMargin: '100px',

  // Пороги видимости (от 0 до 1)
  // 0 = даже пиксель виден
  // 1 = 100% элемента видно
  // [0, 0.5, 1] = срабатывает при 0%, 50%, 100%
  threshold: [0, 0.5, 1],
};

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    // entry.intersectionRatio показывает сколько процентов видно
    console.log(`Видимо: ${entry.intersectionRatio * 100}%`);
  });
}, options);

Практический пример: Анимация при скролле

export function AnimatedSection() {
  const sectionRef = useRef<HTMLDivElement>(null);
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            // Элемент видно, запускаем анимацию
            setIsVisible(true);
            // Необязательно, но можно перестать наблюдать
            observer.unobserve(entry.target);
          }
        });
      },
      {
        threshold: 0.1, // Срабатывает, когда 10% видно
        rootMargin: '-50px', // Срабатывает на 50px раньше
      }
    );

    if (sectionRef.current) {
      observer.observe(sectionRef.current);
    }

    return () => {
      if (sectionRef.current) {
        observer.unobserve(sectionRef.current);
      }
    };
  }, []);

  return (
    <div
      ref={sectionRef}
      className={cn(
        'p-8 bg-surface-1 rounded-lg transition-all duration-700',
        isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'
      )}
    >
      <h2 className="text-2xl font-bold mb-4">Animated Section</h2>
      <p>Эта секция будет анимирована при скролле</p>
    </div>
  );
}

Пример: Отслеживание видимого элемента в списке

export function ScrollSpy() {
  const [activeId, setActiveId] = useState('');

  useEffect(() => {
    // Наблюдаем за всеми section элементами
    const sections = document.querySelectorAll('section');

    const observer = new IntersectionObserver(
      (entries) => {
        // Берём элемент с наибольшей видимостью
        const mostVisible = entries.reduce((prev, current) =>
          current.intersectionRatio > prev.intersectionRatio
            ? current
            : prev
        );

        if (mostVisible.isIntersecting) {
          setActiveId(mostVisible.target.id);
        }
      },
      { threshold: 0.5 }
    );

    sections.forEach((section) => observer.observe(section));

    return () => {
      sections.forEach((section) => observer.unobserve(section));
    };
  }, []);

  return (
    <nav className="sticky top-0 bg-surface-1 p-4">
      <ul className="space-y-2">
        {['intro', 'features', 'pricing', 'contact'].map((id) => (
          <li key={id}>
            <a
              href={`#${id}`}
              className={cn(
                'block p-2 rounded transition',
                activeId === id
                  ? 'bg-surface-2 text-link font-semibold'
                  : 'text-content-2 hover:bg-surface-2'
              )}
            >
              {id.charAt(0).toUpperCase() + id.slice(1)}
            </a>
          </li>
        ))}
      </ul>
    </nav>
  );
}

Сравнение с альтернативами

Intersection Observer vs других подходов:

ПодходПлюсыМинусы
Intersection ObserverОптимально, асинхронно, встроеноIE не поддерживает
Scroll event + getBoundingClientRectПростойСрабатывает часто, блокирует
window.InnerHeight + offsetСтарый методСложный, неточный

Производительность

Intersection Observer — это очень эффективный API:

  • Асинхронный — не блокирует основной поток
  • Батчированный — несколько изменений срабатывают один раз
  • Оптимизированный браузером — V8 знает как это оптимизировать
// Плохо: срабатывает на каждый пиксель скролла
window.addEventListener('scroll', () => {
  document.querySelectorAll('.lazy').forEach((el) => {
    if (el.getBoundingClientRect().top < window.innerHeight) {
      loadImage(el);
    }
  });
});

// Хорошо: асинхронно, только нужные элементы
const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      loadImage(entry.target);
      observer.unobserve(entry.target);
    }
  });
});

document.querySelectorAll('.lazy').forEach((el) => observer.observe(el));

Браузерная поддержка

Intersection Observer поддерживается всеми современными браузерами, кроме IE11. Если нужна поддержка IE, используй полифилл:

npm install intersection-observer
import 'intersection-observer';

Выводы

  • Intersection Observer — мощный инструмент для отслеживания видимости элементов
  • Ленивая загрузка — экономит bandwidth и улучшает performance
  • Бесконечный скролл — улучшает UX для больших списков
  • Асинхронность — не блокирует основной поток
  • Простой API — легко использовать в React через useEffect

Это обязательный API для modern frontend разработчика!