Intersection observer что это

Обновлено: 02.07.2024

Интерфейс IntersectionObserver в составе Intersection Observer API предоставляет возможность асинхронного наблюдения за изменением пересечения целевого элемента с вышестоящим элементом или с верхоуровневым viewport документа. Вышестоящий элемент или viewport считается корнем.

Когда IntersectionObserver создан, он настроен на отслеживание заданных соотношений видимости в корне. Конфигурация не может быть изменена после создания IntersectionObserver , поэтому такой объект-наблюдатель полезен только для наблюдения за определёнными изменениями в степени видимости; однако вы можете следить за несколькими целевыми элементами с одним и тем же наблюдателем.

Intersection Observer API: примеры использования


Intersection Observer API (IOA) позволяет приложению асинхронно наблюдать за пересечением элемента (target) с его родителем (root) или областью просмотра (viewport). Другими словами, этот API обеспечивает вызов определенной функции каждый раз при пересечении целевого элемента с root или viewport.

  • «ленивая» или отложенная загрузка изображений
  • бесконечная прокрутка страницы
  • получение информации о видимости рекламы для целей расчета стоимости показов
  • запуск процесса или анимации, находящихся в поле зрения пользователя
  • root — элемент, который выступает в роли области просмотра для target (предок целевого элемента или null для viewport)
  • rootMargin — отступы вокруг root (margin в CSS, по умолчанию все отступы равны 0)
  • threshold — число или массив чисел, указывающий допустимый процент пересечения target и root


Вызов callback возвращает объект, содержащий записи об изменениях, произошедших с целевым элементом:


В сети полно информации по теории, но довольно мало материалов по практике использования IOA. Я решил немного восполнить этот пробел.

Примеры

«Ленивая» (отложенная) загрузка изображений

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



Результат:

Фон контейнера, выходящего за пределы области просмотра, белый.


При пересечении с областью просмотра наполовину, фон меняется на небесно-голубой.

Замена изображения

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



Результат:

Первое изображение загружено, поскольку находится в области просмотра. Второе — заполнитель.


При дальнейшей прокрутке заполнитель заменяется исходным изображением.

Изменение фона контейнера

Задача: менять фон контейнера при прокрутке страницы пользователем туда и обратно.



Результат:

Фон контейнера меняется от светло-синего…



Работа с видео

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


Пока видео находится в области просмотра, оно проигрывается.


Как только видео выходит за пределы области просмотра больше чем на 40%, его воспроизведение приостанавливается. При попадании в область просмотра > 40% видео, его воспроизведение возобновляется.

Прогресс просмотра страницы

Задача: показывать прогресс просмотра страницы по мере прокрутки страницы пользователем.



При достижении конца страницы в параграф выводится информация о просмотре 4 div.

Бесконечная прокрутка

Задача: реализовать бесконечный список.


Имеем 12 элементов списка. Последний элемент выходит за пределы области просмотра.


При попытке добраться до последнего элемента создается новый (последний) элемент, скрытый от пользователя. И так до бесконечности.

Изменение размеров дочернего элемента при изменении размеров родительского элемента

Задача: установить зависимость размеров одного элемента от другого.



При уменьшении ширины родительского элемента, уменьшается ширина дочернего элемента. При этом расстояние между ними почти всегда равняется 50px («почти» обусловлено реализацией обратного механизма).

Работа с анимацией

Задача: анимировать объект при его видимости.


Мы видим часть головы Барта. Барт прижался к левой стороне области просмотра.


При попадании более 50% Барта в область просмотра, он перемещается на середину. При выходе более 50% Барта за пределы области просмотра, он возвращается в начальное положение.

Реализация ленивой загрузки медиаресурсов

“В чем проблема обычной загрузки медиаресурсов?” — спросите вы. Когда таких ресурсов немного, никакой проблемы нет. Но когда их много и когда компоненты приложения загружаются разом (в приложении не реализован code splitting — разделение кода на уровне компонентов), все ресурсы, используемые этими компонентами, загружаются вместе с ними. Большое количество ресурсов означает "много байт" полезной нагрузки, а много байт полезной нагрузки означает снижение производительности. Следовательно, чем больше весят загружаемые ресурсы, тем хуже производительность приложения.

Как только браузер при разборе разметки встречает атрибут src (например, <img src="https://example.com/some_img.jpg" alt="" role="presentation" />
), он тут же загружает ресурс из указанного в атрибуте источника. Все ресурсы, загружаемые браузером при запуске приложения, можно найти в разделе Network ( Сеть ) инструментов разработчика (ресурсы можно фильтровать с помощью вкладок JS , CSS , Img и др.). Вот, например, Chrome загрузил аватар для моего Google-аккаунта:





Вы можете спросить: "Неужели браузеры не предоставляют какой-либо нативной технологии для реализации ленивой загрузки медиа?" В действительности, кое-что они для этого предоставляют, но… Конечно, есть но, иначе не было бы этой статьи.

Существует атрибут loading . Если добавить его к изображению или фрейму ( iframe ) со значением lazy , то они будут загружаться лениво. И все бы ничего, вот только данный атрибут не поддерживается Safari , а там, где поддерживается, срабатывает не всегда корректно, поэтому для продакшна он не подходит.

Загрузкой аудио и видео можно управлять с помощью атрибута preload . Если установить его в значение none , то медиа будет загружаться только после нажатия пользователем кнопки Play . Данный атрибут поддерживается всеми современными браузерами. Но его возможности являются довольно ограниченными.

Все-таки хотелось бы иметь какой-то общий подход к реализации ленивой загрузки медиа. Кроме того, основной вес большинства страниц составляют изображения.

Для того, чтобы избежать загрузки ресурсов при запуске приложения, необходимо избавиться от всех атрибутов src , но при этом иметь возможность загружать ресурсы… при пересечении соответствующих элементов с viewport ! Здесь нам снова пригодится атрибут data-* . Заменим src на data-src :

Разметка этого приложения будет похожа на разметку предыдущего, за исключением следующего:

  • нам не нужна панель навигации
  • нам хватит 3 разделов
  • между разделами будут находиться медиаресурсы: одно изображение, одно аудио и одно видео.

Думаю, вы уже успели составить себе примерное представление о том, как будет выглядеть код нашего скрипта. Для демонстрации дополнительных возможностей IOA в сравнении с preload , предлагаю запускать автоматическое воспроизведение аудио и видео при пересечении ими порогового значения (которое мы также установим в 30%):

Мы не снимаем наблюдение с элементов посредством вызова метода unobserve в колбэке, поскольку хотим запускать и останавливать воспроизведение аудио и видео при выполнении прокрутки страницы в обоих направлениях. Однако благодаря проверке if (!el.hasAttribute("src")) атрибут data-src меняется на атрибут src только один раз.

Поиграть с этим кодом можно здесь:

До тех пор, пока медиа находятся за пределами области просмотра (проигнорируем пороговое значение подобно константе в большом "O"), они загружаться не будут. В этом мы можем убедиться, изучив вкладку Network .

Изображение находится за пределами viewport :





А вот мы до него "докрутили":





При попадании аудио и видео в область просмотра, начинается их автоматическое воспроизведение, которое автоматически останавливается при выходе соответствующих элементов из viewport .

Рекомендую взглянуть на библиотеку vanilla-lazyload . В дополнение к реализованному нами функционалу, она позволяет лениво загружать фоновые изображения, разные медиаресурсы в зависимости от ширины области просмотра ( source и srcset ) и т.д.

Вывод

В данной статье мы с вами рассмотрели всего лишь 2 примера практического использования IOA . На самом деле, возможности, предоставляемые данным интерфейсом, намного шире. Парочку идей можно найти в этой статье. А здесь можно почитать о других "наблюдателях".

Intersection Observer API: примеры использования


Intersection Observer API (IOA) позволяет приложению асинхронно наблюдать за пересечением элемента (target) с его родителем (root) или областью просмотра (viewport). Другими словами, этот API обеспечивает вызов определенной функции каждый раз при пересечении целевого элемента с root или viewport.

  • «ленивая» или отложенная загрузка изображений
  • бесконечная прокрутка страницы
  • получение информации о видимости рекламы для целей расчета стоимости показов
  • запуск процесса или анимации, находящихся в поле зрения пользователя
  • root — элемент, который выступает в роли области просмотра для target (предок целевого элемента или null для viewport)
  • rootMargin — отступы вокруг root (margin в CSS, по умолчанию все отступы равны 0)
  • threshold — число или массив чисел, указывающий допустимый процент пересечения target и root


Вызов callback возвращает объект, содержащий записи об изменениях, произошедших с целевым элементом:


В сети полно информации по теории, но довольно мало материалов по практике использования IOA. Я решил немного восполнить этот пробел.

Примеры

«Ленивая» (отложенная) загрузка изображений

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



Результат:

Фон контейнера, выходящего за пределы области просмотра, белый.


При пересечении с областью просмотра наполовину, фон меняется на небесно-голубой.

Замена изображения

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



Результат:

Первое изображение загружено, поскольку находится в области просмотра. Второе — заполнитель.


При дальнейшей прокрутке заполнитель заменяется исходным изображением.

Изменение фона контейнера

Задача: менять фон контейнера при прокрутке страницы пользователем туда и обратно.



Результат:

Фон контейнера меняется от светло-синего…



Работа с видео

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


Пока видео находится в области просмотра, оно проигрывается.


Как только видео выходит за пределы области просмотра больше чем на 40%, его воспроизведение приостанавливается. При попадании в область просмотра > 40% видео, его воспроизведение возобновляется.

Прогресс просмотра страницы

Задача: показывать прогресс просмотра страницы по мере прокрутки страницы пользователем.



При достижении конца страницы в параграф выводится информация о просмотре 4 div.

Бесконечная прокрутка

Задача: реализовать бесконечный список.


Имеем 12 элементов списка. Последний элемент выходит за пределы области просмотра.


При попытке добраться до последнего элемента создается новый (последний) элемент, скрытый от пользователя. И так до бесконечности.

Изменение размеров дочернего элемента при изменении размеров родительского элемента

Задача: установить зависимость размеров одного элемента от другого.



При уменьшении ширины родительского элемента, уменьшается ширина дочернего элемента. При этом расстояние между ними почти всегда равняется 50px («почти» обусловлено реализацией обратного механизма).

Работа с анимацией

Задача: анимировать объект при его видимости.


Мы видим часть головы Барта. Барт прижался к левой стороне области просмотра.


При попадании более 50% Барта в область просмотра, он перемещается на середину. При выходе более 50% Барта за пределы области просмотра, он возвращается в начальное положение.

Реализация навигации по разделам сайта

Элементы меню (ссылки) соответствуют разделам страницы (заголовкам разделов, если быть точнее). Каждому заголовку соответствует определенная ссылка. Мы это знаем, но этого не знает JS . Поэтому нам нужно как-то сообщить ему об этих отношениях один к одному . Существует несколько способов это сделать, но самым простым является использование атрибута data-* . Назовем его data-section . Пускай для простоты значениями этих атрибутов будут номера разделов (1, 2, 3 и т.д.).

Также нам нужен какой-то способ визуального отображения текущего раздела, т.е. раздела, просматриваемого пользователем. Самым простым способом это сделать является добавление к такому разделу специального CSS-класса. Назовем этот класс active .

Вот как будет выглядеть один элемент навигации и соответствующий ему раздел:

Таких элементов и разделов у нас будет 5 штук.

Стили вполне себе обычные, так что на них я останавливаться не буду.

Как вы думаете, сколько строк JS-кода занимает решение данной задачи? Не забывайте о том, что кроме выделения элемента навигации, соответствующего текущему разделу, с помощью стилей, нам также необходимо реализовать переключение между разделами по клику (при этом визуализация должна отрабатывать корректно). Так сколько же? 100? 50?

Ответ: 30 строк! И это без экономии на пробелах и отступах.

Завернем весь наш код в функцию. Назовем ее createSectionNav . Пусть функция принимает корневой элемент — root :

Получаем ссылки на nav , main и текущий (активный) элемент навигации (далее — шаг):

Сначала реализуем переключение между разделами по клику. Для прокрутки к определенному элементу мы воспользуемся глобальным методом scrollIntoView . А находить нужный элемент будем с помощью атрибута data-section :

В принципе, если на странице много разделов, для того, чтобы каждый раз не искать нужный раздел с помощью querySelector , можно предварительно составить карту разделов:

И обращаться к соответствующему разделу по ключу:

Это все, что требуется для переключения между разделами. “А как же визуальная составляющая?” — спросите вы. О, это самое интересное. Визуальная составляющая будет полностью инкапсулирована в наблюдателе.

Из настроек нам требуется только процент пересечения. Установим его в значение 0.3 (30%):

Функция обратного вызова делает следующее:

  • принимает список объектов entries
  • извлекает первый entry
  • проверяет, находится ли он зоне пересечения
  • если находится
    • извлекает номер раздела из его data-section
    • удаляет класс active у текущего активного элемента
    • определяет новый активный элемент
    • добавляет к нему класс active
    • и присваивает новое значение переменной currentActiveStep

    И начинаем следить за разделом:

    Если все это объединить, то получится следующее:

    Таким образом, визуальная составляющая полностью инкапсулирована в наблюдателе: колбэк запускается как при "ручной" прокрутке страницы, так и при клике по шагу — в этом случае выполняется автоматическая прокрутка, также приводящая к регистрации пересечения.

    Поиграть с этим кодом можно здесь:

    Теперь поговорим о ленивой загрузке.

    Введение

    Итак, задачи были следующими:

    • Реализовать навигацию по разделам сайта с визуальным переключением индикатора текущего местонахождения пользователя и возможностью переходить к определенному разделу по клику. Сама навигация согласно макету должна была выглядеть так:
    • Реализовать "ленивую" (отложенную, lazy) загрузку медиаресурсов (изображений, аудио и видео), поскольку те же изображения даже после сжатия с помощью gulp-imagemin весили (и весят, потому что они никуда не делись) неприличные 50 Мб и загружались при запуске приложения (больше они себя так не ведут).

    Как я уже сказал, обе задачи надо было решить на ванильном JS . Почему? Потому что проект был, что называется, legacy — Python в лице Wagtail и JS в лице JQuery .

    На первый взгляд, может показаться, что названные задачи являются совершенно разными и между ними не может быть ничего общего. “Что может их объединять?” — спросите вы.

    Ответ: технология, которую можно использовать для их решения. Вы не ослышались, обе задачи можно решить с помощью одного инструмента и при этом его даже не придется устанавливать. Для решения задач нам не потребуются ни сторонние библиотеки, ни вспомогательные утилиты — интерфейс, о котором идет речь, предоставляется самим браузером.

    Как вы уже могли догадаться по названию статьи, я говорю об Intersection Observer API (далее — IOA ). Данный интерфейс поддерживается всеми современными браузерами.

    Если кратко, то IOA позволяет асинхронно регистрировать момент пересечения целевого элемента с его предком, если таковой определен в настройках, или с областью видимости документа ( viewport ).

    Экземпляр IOA создается так:

    Конструктор IntersectionObserver принимает 2 параметра: функцию обратного вызова, запускаемую в момент пересечения — callback , и объект с настройками — options .

    Объект с настройками может содержать 3 поля:

    • root — родительский элемент, за пересечением с которым (целевым элементом) наблюдает наблюдатель (извините за тавтологию). Если root не определен или имеет значение null , предком целевого элемента считается viewport
    • rootMargin — отступы вокруг root . Как и для CSS-свойства margin , значения для rooMargin могут задаваться в пикселях, процентах, em, rem и т.д., например, 20px 10px . По умолчанию данная настройка имеет значение 0
    • threshold — процент видимости целевого элемента от 0 до 1 . Значение данной настройки может задаваться в виде числа или массива чисел. По умолчанию также имеет значение 0

    Наблюдение за целевым элементом устанавливается посредством вызова метода observe :

    Существует также несколько других методов, но мы будет использовать только observe .

    При достижении целевым элементом порогового значения ( threshold ) вызывается callback , который получает список объектов entries и самого наблюдателя. Каждый объект entry имеет несколько свойств ( target , time , intersectionRatio и др.). Единственным свойством, которое нас интересует, является isIntersection — логическое значение, выступающее индикатором нахождения целевого элемента в зоне пересечения.

    Кстати, процент пересечения также можно определять с помощью свойства intersectionRatio .

    Пожалуй, это все, что нам нужно знать об IOA для того, чтобы успешно решить наши задачи. Приступаем.

    Начнем с навигации.

    JavaScript: 2 интересных примера практического использования Intersection Observer API



    На днях мне посчастливилось заниматься решением 2 несложных, но довольно интересных задач на чистом JavaScript (из-за React чуть не забыл, как это делается). В процессе решения этих задач никто не пострадал, напротив, все остались довольны. Поэтому я решил поделиться результатами с сообществом.

    Обратите внимание: данная статья рассчитана, преимущественно, на начинающих разработчиков и вряд ли покажется интересной опытным, хотя, смею надеяться, что и последние не пожалеют потраченного времени, если прочитают ее до конца.

    Основные понятия

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

    В основном, используется отслеживание пересечения элемента с областью видимости (необходимо указать null в качестве корневого элемента).

    Используете ли вы область видимости или другой элемент в качестве корневого, API работает одинаково, вызывая заданную вами колбэк-функцию, всякий раз, когда видимость целевого элемента изменяет так, что она пересекает в нужной степени корневой элемент.

    Степень пересечения целевого и корневого элемента задаётся в диапазоне от 0.0 до 1.0, где 1.0 это полное пересечение целевого элемента границ корневого.

    Пример использования

    Для начала с помощью конструктора нужно создать объект-наблюдатель, указать для него функцию для вызова и настройки отслеживания:

    Параметр threshold со значением 1.0 означает что функция будет вызвана при 100% пересечении объекта (за которым мы следим) с объектом root

    Настройки

    root Элемент который используется в качестве области просмотра для проверки видимости целевого элемента. Должен быть предком целевого элемента. По умолчанию используется область видимости браузера если не определён или имеет значение null . rootMargin Отступы вокруг root. Могут иметь значения как свойство css margin: " 10px 20px 30px 40px" (top, right, bottom, left). Значения можно задавать в процентах. По умолчанию все параметры установлены в нули. threshold Число или массив чисел, указывающий, при каком проценте видимости целевого элемента должен сработать callback. Например, в этом случае callback функция будет вызываться при появлении в зоне видимости каждый 25% целевого элемента: [0, 0.25, 0.5, 0.75, 1]
    Целевой элемент, который будет наблюдаться

    После того, как вы создали наблюдателя, вам нужно дать ему целевой элемент для просмотра:

    Всякий раз, когда цель достигает порогового значения, указанного для IntersectionObserver , вызывается колбэк-функция callback . Где callback получает список объектов IntersectionObserverEntry (en-US) и наблюдателя:

    Обратите внимание, что колбэк-функция запускается в главном потоке и должна выполняться как можно быстрее, поэтому если что-то отнимает много времени, то используйте Window.requestIdleCallback() .

    Также обратите внимание, что если вы указали опцию root , целевой элемент должен быть потомком корневого элемента.

    Intersection Observer API

    Intersection Observer API позволяет веб-приложениям асинхронно следить за изменением пересечения элемента с его родителем или областью видимости документа viewport.

    Исторически обнаружение видимости отдельного элемента или видимости двух элементов по отношению друг к другу было непростой задачей. Варианты решения этой задачи были ненадёжными и замедляли работу браузера. К несчастью, по мере того как веб "взрослел", потребность в решении этой проблемы только росла по многим причинам, таким как:

    • Отложенная загрузка изображений или другого контента по мере прокрутки страницы.
    • Реализация веб-сайтов с "бесконечным скроллом", где контент подгружается по мере того как страница прокручивается вниз, и пользователю не нужно переключаться между страницами.
    • Отчёт о видимости рекламы с целью посчитать доходы от неё.
    • Принятие решения, запускать ли какой-то процесс или анимацию в зависимости от того, увидит пользователь результат или нет.

    В прошлом реализация обнаружения пересечения элементов подразумевала использование обработчиков событий и циклов, вызывающих методы типа Element.getBoundingClientRect() , чтобы собрать необходимую информацию о каждом затронутом элементе. Поскольку весь этот код работает в основном потоке, возникают проблемы с производительностью.

    Рассмотрим веб-страницу с бесконечным скроллом. На ней используется библиотека для управления периодически размещаемой по всей странице рекламой, повсюду анимированная графика, а также библиотека для отображения всплывающих окон. И все эти вещи используют свои собственные правила для обнаружения пересечений, и все они запущены в основном потоке. Автор сайта может даже не подозревать об этой проблеме, а также может не знать, как работают сторонние библиотеки изнутри. В то же время пользователь по ходу прокрутки страницы сталкивается с тем, что работа сайта замедляется постоянным срабатыванием обнаружения пересечения, что в итоге приводит к тому, что пользователь недоволен браузером, сайтом и своим компьютером.

    Intersection Observer API даёт возможность зарегистрировать колбэк-функцию, которая выполнится при пересечении наблюдаемым элементом границ другого элемента (или области видимости документа viewport), либо при изменении величины пересечения на определённое значение. Таким образом, больше нет необходимости вычислять пересечение элементов в основном потоке, и браузер может оптимизировать эти процессы на своё усмотрение.

    Observer API не позволит узнать точное число пикселей или определить конкретные пиксели в пересечении; однако, его использование покрывает наиболее частые сценарии вроде "Если элементы пересекаются на N%, сделай то-то".

    Читайте также: