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

Как понять, что элемент находится во ViewPort?

1.7 Middle🔥 151 комментариев
#HTML и CSS

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

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

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

ViewPort: Определение видимости элемента

ViewPort — это область экрана браузера, которая видна пользователю. Понимание, находится ли элемент во ViewPort, необходимо для ленивой загрузки изображений, infinite scroll и других оптимизаций.

Что такое ViewPort

┌─────────────────────────┐
│                         │
│   VIEWPORT (видно)      │
│                         │
│  ┌─────────────────┐    │
│  │                 │    │ <- Элемент ВНУТРИ viewport
│  │    Element      │    │
│  │                 │    │
│  └─────────────────┘    │
│                         │
└─────────────────────────┘

┌─────────────────────────┐
│  ┌─────────────────┐    │
│  │                 │    │
│  │    Element      │    │ <- Элемент ЧАСТИЧНО внутри
│  │                 │    │
│  └─────────────────┘    │
└─────────────────────────┘
       
     ┌─────────────────┐
     │                 │
     │    Element      │     <- Элемент ВНЕ viewport
     │                 │
     └─────────────────┘

Метод 1: getBoundingClientRect()

Самый простой способ — получить координаты элемента относительно viewport:

function isElementInViewPort(element) {
  const rect = element.getBoundingClientRect();
  
  return (
    rect.top >= 0 &&
    rect.left >= 0 &&
    rect.bottom <= window.innerHeight &&
    rect.right <= window.innerWidth
  );
}

const elem = document.querySelector('.my-element');
console.log(isElementInViewPort(elem)); // true / false

Объект DOMRect содержит:

  • top — расстояние от верхней границы viewport
  • left — расстояние от левой границы viewport
  • bottom — расстояние от верхней границы viewport + высота элемента
  • right — расстояние от левой границы viewport + ширина элемента
  • height — высота элемента
  • width — ширина элемента

Пример: Частично видимый элемент

function isElementPartiallyInViewPort(element) {
  const rect = element.getBoundingClientRect();
  
  return (
    rect.bottom > 0 &&
    rect.right > 0 &&
    rect.top < window.innerHeight &&
    rect.left < window.innerWidth
  );
}

const elem = document.querySelector('.my-element');
console.log(isElementPartiallyInViewPort(elem)); // Видно ли хоть часть?

Пример: С offset для предзагрузки

function isElementNearViewPort(element, offset = 100) {
  const rect = element.getBoundingClientRect();
  
  return (
    rect.bottom > -offset &&
    rect.right > -offset &&
    rect.top < window.innerHeight + offset &&
    rect.left < window.innerWidth + offset
  );
}

// Загружать изображение за 100px до появления в viewport
const img = document.querySelector('img');
if (isElementNearViewPort(img, 100)) {
  img.src = img.dataset.src; // Загружаем изображение
}

Метод 2: Intersection Observer API (РЕКОМЕНДУЕТСЯ)

Современный способ — более эффективный и простой в использовании:

const element = document.querySelector('.my-element');

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      console.log('Element is in viewport!');
    } else {
      console.log('Element is NOT in viewport');
    }
  });
});

observer.observe(element);

// Остановить наблюдение
// observer.unobserve(element);
// observer.disconnect();

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

const images = document.querySelectorAll('img[data-src]');

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); // Больше не смотрим
    }
  });
});

images.forEach((img) => imageObserver.observe(img));

HTML:

<img data-src="image1.jpg" alt="" />
<img data-src="image2.jpg" alt="" />
<img data-src="image3.jpg" alt="" />

Пример: Инфинитный скролл

const sentinel = document.querySelector('.sentinel');

const scrollObserver = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      console.log('User scrolled to bottom — load more items!');
      loadMoreItems();
    }
  });
});

scrollObserver.observe(sentinel); // sentinel — последний элемент списка

Опции Intersection Observer

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      console.log('Visibility:', entry.intersectionRatio); // 0 до 1
    });
  },
  {
    root: document.querySelector('.scroll-container'), // Родитель (viewport)
    rootMargin: '100px', // Дополнительный margin вокруг root
    threshold: 0.5 // Триггер при 50% видимости
  }
);

Параметры:

  • root — элемент, который считается viewport (по умолчанию — window)
  • rootMargin — расширить область видимости (как padding)
  • threshold — при скольких процентах видимости триггерить callback (0-1 или массив)

Пример: Разные пороги

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (entry.intersectionRatio > 0.75) {
        entry.target.classList.add('mostly-visible');
      } else if (entry.intersectionRatio > 0.25) {
        entry.target.classList.add('partially-visible');
      } else {
        entry.target.classList.remove('mostly-visible', 'partially-visible');
      }
    });
  },
  {
    threshold: [0, 0.25, 0.5, 0.75, 1] // Проверять эти пороги
  }
);

Метод 3: Слушатель scroll + getBoundingClientRect()

Старый способ (менее эффективный, но все ещё используется):

function checkIfInViewPort() {
  const element = document.querySelector('.my-element');
  const rect = element.getBoundingClientRect();
  
  const isInViewPort = rect.bottom > 0 && rect.top < window.innerHeight;
  
  if (isInViewPort) {
    console.log('Element is in viewport');
  }
}

// Проверять при каждом скролле
window.addEventListener('scroll', checkIfInViewPort);

// Или с debounce для оптимизации
import { debounce } from 'lodash';

const debouncedCheck = debounce(checkIfInViewPort, 100);
window.addEventListener('scroll', debouncedCheck);

Минусы:

  • Триггерится много раз при скролле
  • Нужно самостоятельно оптимизировать (debounce, throttle)
  • Более сложно в коде

Метод 4: scrollIntoView()

Прокрутить до элемента в viewport:

const element = document.querySelector('.my-element');

// Мгновенная прокрутка
element.scrollIntoView();

// Гладкая прокрутка
element.scrollIntoView({ behavior: 'smooth', block: 'start' });

// Параметры
// behavior: 'auto' | 'smooth'
// block: 'start' | 'center' | 'end' | 'nearest'
// inline: 'start' | 'center' | 'end' | 'nearest'

Практические примеры

Пример 1: Analytics — отслеживание просмотров

interface TrackingConfig {
  threshold: number;
  onView: (element: Element) => void;
}

function trackElementViews(selector: string, config: TrackingConfig) {
  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting && entry.intersectionRatio >= config.threshold) {
          config.onView(entry.target);
          observer.unobserve(entry.target); // One-time tracking
        }
      });
    },
    { threshold: config.threshold }
  );

  document.querySelectorAll(selector).forEach((el) => observer.observe(el));
}

// Использование
trackElementViews('.tracked-element', {
  threshold: 0.5,
  onView: (el) => {
    const productId = el.dataset.productId;
    analytics.track('product_viewed', { productId });
  }
});

Пример 2: Lazy loading с fallback

function lazyLoadImages() {
  const images = document.querySelectorAll('img[data-src]');

  // Используем Intersection Observer, если доступен
  if ('IntersectionObserver' in window) {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          const img = entry.target as HTMLImageElement;
          img.src = img.dataset.src!;
          img.onload = () => img.classList.add('loaded');
          observer.unobserve(img);
        }
      });
    });

    images.forEach((img) => observer.observe(img));
  } else {
    // Fallback для старых браузеров
    images.forEach((img) => {
      img.src = (img as HTMLImageElement).dataset.src!;
    });
  }
}

Пример 3: Sticky header появляется при скролле

const sentinel = document.querySelector('.header-sentinel');
const stickyHeader = document.querySelector('.sticky-header');

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (!entry.isIntersecting) {
        stickyHeader.classList.add('show');
      } else {
        stickyHeader.classList.remove('show');
      }
    });
  },
  { threshold: 0 }
);

observer.observe(sentinel);

Сравнение методов

МетодПроизводительностьПростотаПоддержка
getBoundingClientRectСредняяПростаяВсе браузеры
Intersection ObserverОтличнаяПростаяСовременные браузеры
scroll listenerПлохаяСложнаяВсе браузеры
scrollIntoView-Очень простаяБольшинство

Заключение

Для определения видимости элемента во ViewPort:

  1. Современный способ: Intersection Observer API (рекомендуется)

    • Эффективнее всего
    • Проще в коде
    • Встроенные опции для тонкой настройки
  2. Традиционный способ: getBoundingClientRect()

    • Подходит для старых браузеров
    • Нужна оптимизация (debounce на scroll)
  3. Старый способ: scroll listener + getBoundingClientRect()

    • Избегать в новом коде
    • Много false-positive срабатываний

Используй Intersection Observer для:

  • Ленивой загрузки изображений
  • Инфинитного скролла
  • Аналитики
  • Анимаций при появлении элемента
Как понять, что элемент находится во ViewPort? | PrepBro