> Как определить, что элемент находится в viewport в JavaScript (JavaScript)

Уровень: senior · Роль: frontend · Категория: Технические вопросы

Компании: Домклик

Стек: JavaScript

> Пример ответа

Короткий ответ

Основной способ - использовать getBoundingClientRect() и сравнить координаты элемента с размерами viewport. Альтернатива для современных браузеров - IntersectionObserver, который работает асинхронно и эффективнее при отслеживании множества элементов. Выбор зависит от задачи: для разовой проверки подходит getBoundingClientRect(), для постоянного мониторинга (lazy loading, анимации) - IntersectionObserver.

Подробное объяснение

getBoundingClientRect() возвращает объект с координатами элемента относительно viewport: top, bottom, left, right. Элемент видим, если его границы пересекаются с областью просмотра:

JAVASCRIPT
const rect = el.getBoundingClientRect();
const isVisible = rect.top < window.innerHeight && rect.bottom > 0 &&
rect.left < window.innerWidth && rect.right > 0;

Важно учитывать, что getBoundingClientRect() вызывает reflow - при массовых проверках это может быть дорого. Также он не учитывает opacity: 0, visibility: hidden или display: none - элемент с такими стилями всё равно вернёт координаты.

IntersectionObserver решает эти проблемы: он работает асинхронно, не блокирует main thread, и позволяет задать порог видимости (threshold) и корневой элемент (root). При этом он не поддерживается в старых браузерах (IE11), поэтому для legacy-проектов нужен полифилл или fallback на getBoundingClientRect().

Дополнительные нюансы: при наличии горизонтального скролла нужно учитывать window.innerWidth, а не document.documentElement.clientWidth - последний не включает scrollbar. Для iframe viewport - это окно iframe, а не родительской страницы.

На практике

Для lazy loading изображений предпочтителен IntersectionObserver - он позволяет отменить наблюдение после срабатывания и не требует ручной обработки scroll-событий. Для разовой проверки при клике или в обработчике - getBoundingClientRect() проще и не требует создания observer.

При использовании IntersectionObserver стоит помнить: callback срабатывает асинхронно, поэтому нельзя полагаться на него в момент, когда нужно синхронно получить результат. Также observer не сработает, если элемент уже в viewport на момент создания - нужно вызвать observe() и дождаться первого callback.

Для производительности при scroll-событиях с getBoundingClientRect() используйте requestAnimationFrame или throttle, чтобы не вызывать reflow чаще, чем раз в кадр.

Пример кода

JAVASCRIPT
// Проверка через getBoundingClientRect
function isElementInViewport(el, partiallyVisible = true) {
const rect = el.getBoundingClientRect();
const vw = window.innerWidth || document.documentElement.clientWidth;
const vh = window.innerHeight || document.documentElement.clientHeight;
if (partiallyVisible) {
return rect.top < vh && rect.bottom > 0 && rect.left < vw && rect.right > 0;
}
return rect.top >= 0 && rect.left >= 0 &&
rect.bottom <= vh && rect.right <= vw;
}
// Использование IntersectionObserver
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// элемент появился в viewport
entry.target.classList.add('visible');
observer.unobserve(entry.target); // отписываемся после срабатывания
}
});
}, {
root: null, // viewport
threshold: 0.1 // 10% элемента должно быть видно
});
observer.observe(document.querySelector('.lazy-image'));

Как отвечать на собеседовании

Начните с getBoundingClientRect() как базового решения, объясните логику сравнения координат. Затем упомяните IntersectionObserver как более современный и производительный подход. Подчеркните trade-off: первый - синхронный и простой, второй - асинхронный и эффективный для множества элементов. Упомяните про reflow и необходимость throttle для scroll-обработчиков. Если спросят про edge cases - расскажите про iframe, scrollbar, display: none и пороги видимости.

Что проверяет интервьюер

Интервьюер оценивает понимание работы браузерного рендеринга, знание API и умение выбирать инструмент под задачу. Важно показать осознание производительности: почему getBoundingClientRect() в scroll-обработчике - плохая идея, и как IntersectionObserver решает эту проблему. Также проверяется знание ограничений обоих подходов и умение аргументировать выбор.

Типичные ошибки

  • Использование document.documentElement.clientHeight вместо window.innerHeight - при наличии горизонтального scrollbar значения различаются.
  • Забывают про горизонтальную прокрутку и проверяют только top и bottom.
  • Вызов getBoundingClientRect() в scroll-обработчике без throttle - вызывает reflow на каждое событие.
  • Не учитывают, что IntersectionObserver не срабатывает мгновенно - результат приходит асинхронно.
  • Путают isIntersecting со значением intersectionRatio - первое булево, второе число от 0 до 1.
  • Проверяют только display: none, но не visibility: hidden или opacity: 0 - эти элементы всё равно считаются видимыми по координатам.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью