Теперь вы видите меня: как отложить, отложить загрузку и действовать с IntersectionObserver

Опубликовано: 2022-03-10
Краткий обзор ↬ Информация о пересечении необходима по многим причинам, например, для ленивой загрузки изображений. Но есть и многое другое. Пришло время лучше понять Intersection Observer API и взглянуть на него по-новому. Готовый?

Давным-давно жил веб-разработчик, который успешно убедил своих клиентов в том, что сайты не должны выглядеть одинаково во всех браузерах, позаботился о доступности и одним из первых внедрил сетки CSS. Но в глубине души его истинной страстью была производительность: он постоянно оптимизировал, минимизировал, контролировал и даже использовал психологические приемы в своих проектах.

Затем, однажды, он узнал об отложенной загрузке изображений и других ресурсов, которые не сразу видны пользователям и не являются необходимыми для отображения значимого контента на экране. Это было начало рассвета: разработчик вошел в злой мир плагинов jQuery с отложенной загрузкой (или, может быть, в не такой уж злой мир атрибутов async и defer ). Некоторые даже говорят, что он попал прямо в сердцевину всех зол: мир прослушивателей событий scroll . Мы никогда не узнаем наверняка, где он оказался, но опять же, этот разработчик абсолютно вымышленный, и любое сходство с любым разработчиком просто случайно.

веб-разработчик
Вымышленный веб-разработчик

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

Еще после прыжка! Продолжить чтение ниже ↓

В этой статье мы собираемся выйти из тьмы scroll и поговорить о современном способе ленивой загрузки ресурсов. Не только отложенная загрузка изображений, но и загрузка любого актива в этом отношении. Более того, метод, о котором мы сегодня поговорим, способен на гораздо большее, чем просто ленивая загрузка ресурсов: мы сможем предоставить любой тип отложенной функциональности на основе видимости элементов для пользователей.

IntersectionObserver: теперь ты видишь меня

Дамы и господа, давайте поговорим об API Intersection Observer. Но прежде чем мы начнем, давайте взглянем на ландшафт современных инструментов, который привел нас к IntersectionObserver .

2017 год был очень удачным для инструментов, встроенных в наши браузеры, которые помогли нам улучшить качество и стиль нашей кодовой базы без особых усилий. В наши дни Интернет, похоже, отходит от спорадических решений, основанных на очень разных решениях, к решениям, очень типичным для более четко определенного подхода интерфейсов Observer (или просто «наблюдателей»): хорошо поддерживаемый MutationObserver получил новых членов семьи, которые быстро принято в современных браузерах:

  • IntersectionObserver и
  • PerformanceObserver (как часть спецификации Performance Timeline Level 2).

Еще один потенциальный член семьи, FetchObserver, находится в стадии разработки и ведет нас больше в мир сетевых прокси, но сегодня я хотел бы вместо этого больше поговорить о внешнем интерфейсе.

IntersectionObserver и PerformanceObserver — новые члены семейства Observers.
IntersectionObserver и PerformanceObserver — новые члены семейства Observers.

PerformanceObserver и IntersectionObserver нацелены на то, чтобы помочь разработчикам интерфейса улучшить производительность своих проектов на разных этапах. Первый дает нам инструмент для мониторинга реальных пользователей, а второй — инструмент, обеспечивающий ощутимое улучшение производительности. Как упоминалось ранее, в этой статье подробно рассматривается именно последний: IntersectionObserver . Чтобы понять, в частности, механику IntersectionObserver , мы должны взглянуть на то, как общий Observer должен работать в современной сети.

Совет для профессионалов : вы можете пропустить теорию и сразу погрузиться в механику IntersectionObserver или, даже дальше, сразу к возможным применениям IntersectionObserver .

Наблюдатель против события

«Наблюдатель», как следует из названия, предназначен для наблюдения за чем-то, что происходит в контексте страницы. Наблюдатели могут наблюдать за тем, что происходит на странице, например за изменениями DOM. Они также могут отслеживать события жизненного цикла страницы. Наблюдатели также могут запускать некоторые функции обратного вызова. Теперь внимательный читатель может сразу заметить здесь проблему и спросить: «Ну и в чем смысл? Разве у нас уже нет мероприятий для этой цели? Что отличает Observers?» Очень хороший момент! Давайте посмотрим поближе и разберемся.

Наблюдатель и событие: в чем разница?
Наблюдатель и событие: в чем разница?

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

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

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

Так что, если кому-то сложно отойти от парадигмы Event, я бы сказал, что Observers — это события на стероидах. Другое описание может быть таким: Наблюдатели — это новый уровень приближения к событиям. Но независимо от того, какое определение вы предпочитаете, само собой разумеется, что наблюдатели не предназначены для замены событий (по крайней мере, пока); есть достаточно вариантов использования для обоих, и они могут счастливо жить бок о бок.

Наблюдатели не предназначены для замены событий: оба могут счастливо жить вместе.
Наблюдатели не предназначены для замены событий: оба могут счастливо жить вместе.

Общая структура наблюдателя

Общая структура Observer (любая из доступных на момент написания статьи) выглядит примерно так:

 /** * Typical Observer's registration */ let observer = new YOUR-TYPE-OF-OBSERVER(function (entries) { // entries: Array of observed elements entries.forEach(entry => { // Here we can do something with each particular entry }); }); // Now we should tell our Observer what to observe observer.observe(WHAT-TO-OBSERVE);

Опять же, обратите внимание, что entries представляют собой Array значений, а не одну запись.

Это общая структура: реализации конкретных наблюдателей различаются аргументами, передаваемыми в его observe() , и аргументами, передаваемыми в его обратный вызов. Например, MutationObserver также должен получить объект конфигурации, чтобы узнать больше о том, какие изменения в DOM следует наблюдать. PerformanceObserver не наблюдает за узлами в DOM, но вместо этого имеет выделенный набор типов записей, которые он может наблюдать.

На этом давайте закончим «общую» часть этого обсуждения и углубимся в тему сегодняшней статьи — IntersectionObserver .

Деконструкция IntersectionObserver

Деконструкция IntersectionObserver
Деконструкция IntersectionObserver

Прежде всего, давайте разберемся, что такое IntersectionObserver .

Согласно МДН:

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

Проще говоря, IntersectionObserver асинхронно отслеживает наложение одного элемента другим элементом. Давайте поговорим о том, для чего нужны эти элементы в IntersectionObserver .

Инициализация IntersectionObserver

В одном из предыдущих абзацев мы видели структуру универсального наблюдателя. IntersectionObserver немного расширяет эту структуру. Прежде всего, для этого типа Observer требуется конфигурация с тремя основными элементами:

  • root : это корневой элемент, используемый для наблюдения. Он определяет базовую «рамку захвата» для наблюдаемых элементов. По умолчанию root является область просмотра вашего браузера, но на самом деле это может быть любой элемент в вашей модели DOM (тогда вы устанавливаете для root что-то вроде document.getElementById('your-element') ). Имейте в виду, однако, что в этом случае элементы, которые вы хотите наблюдать, должны «жить» в root дереве DOM.
свойство root конфигурации IntersectionObserver
свойство root определяет основу для «захвата кадра» для наших элементов.
  • rootMargin : определяет поле вокруг вашего root элемента, которое расширяет или сжимает «рамку захвата», когда размеры вашего root не обеспечивают достаточной гибкости. Варианты значений этой конфигурации аналогичны параметрам margin в CSS, например, rootMargin: '50px 20px 10px 40px' (сверху, справа внизу, слева). Значения могут быть сокращены (например rootMargin: '50px' ) и могут быть выражены либо в px , либо в % . По умолчанию rootMargin: '0px' .
Свойство rootMargin конфигурации IntersectionObserver
rootMargin расширяет/сжимает «кадр захвата», который определяется root .
  • threshold : не всегда желательно реагировать мгновенно, когда наблюдаемый элемент пересекает границу «кадра захвата» (определяется как комбинация root и rootMargin ). threshold определяет процент такого пересечения, при котором наблюдатель должен реагировать. Он может быть определен как одно значение или как массив значений. Чтобы лучше понять эффект threshold (я знаю, что иногда это может сбивать с толку), вот несколько примеров:
    • threshold: 0 : значение по умолчанию IntersectionObserver должен реагировать, когда самый первый или самый последний пиксель наблюдаемого элемента пересекает одну из границ «кадра захвата». Имейте в виду, что IntersectionObserver не зависит от направления, а это означает, что он будет реагировать в обоих сценариях: а) когда элемент входит и б) когда он покидает «кадр захвата».
    • threshold: 0.5 : наблюдатель должен срабатывать, когда 50% наблюдаемого элемента пересекает «кадр захвата»;
    • threshold: [0, 0.2, 0.5, 1] ​​: Наблюдатель должен реагировать в 4 случаях:
      • Самый первый пиксель наблюдаемого элемента попадает в «кадр захвата»: элемент на самом деле все еще не находится в этом кадре, или самый последний пиксель наблюдаемого элемента покидает «кадр захвата»: элемент больше не находится в кадре;
      • 20% элемента находится в пределах «кадра захвата» (опять же, для IntersectionObserver направление не имеет значения);
      • 50% элемента находится в «рамке захвата»;
      • 100% элемента находится в «рамке захвата». Это строго противоположно threshold: 0 .
пороговое свойство конфигурации IntersectionObserver
Свойство threshold определяет, насколько элемент должен пересекать наш «кадр захвата», прежде чем наблюдатель сработает.

Чтобы сообщить нашему IntersectionObserver о желаемой конфигурации, мы просто передаем наш объект config в конструктор нашего Observer вместе с нашей функцией обратного вызова следующим образом:

 const config = { root: null, // avoiding 'root' or setting it to 'null' sets it to default value: viewport rootMargin: '0px', threshold: 0.5 }; let observer = new IntersectionObserver(function(entries) { … }, config);

Теперь мы должны предоставить IntersectionObserver фактический элемент для наблюдения. Это делается просто путем передачи элемента в observe() :

 … const img = document.getElementById('image-to-observe'); observer.observe(image);

Несколько замечаний об этом наблюдаемом элементе:

  • Об этом упоминалось ранее, но стоит упомянуть еще раз: если вы устанавливаете root как элемент в DOM, наблюдаемый элемент должен находиться в дереве DOM root .
  • IntersectionObserver может одновременно принимать только один элемент для наблюдения и не поддерживает пакетную поставку для наблюдений. Это означает, что если вам нужно наблюдать за несколькими элементами (скажем, несколькими изображениями на странице), вы должны перебирать их все и наблюдать за каждым из них отдельно:
 … const images = document.querySelectorAll('img'); images.forEach(image => { observer.observe(image); });
  • При загрузке страницы с установленным Observer вы можете заметить, что обратный вызов IntersectionObserver был запущен для всех наблюдаемых элементов одновременно. Даже те, которые не соответствуют поставляемой конфигурации. «Ну… не совсем то, что я ожидал», — обычная мысль, когда вы испытываете это в первый раз. Но не запутайтесь здесь: это не обязательно означает, что эти наблюдаемые элементы каким-то образом пересекают «кадр захвата» во время загрузки страницы.
Снимок экрана DevTools с запуском IntersectionObserver для всех элементов одновременно.
IntersectionObserver будет запущен для всех наблюдаемых элементов после их регистрации, но это не означает, что все они пересекаются с нашим «кадром захвата».

Однако это означает, что запись для этого элемента была инициализирована и теперь контролируется вашим IntersectionObserver . Однако это может добавить ненужного шума в вашу функцию обратного вызова, и вы должны определить, какие элементы действительно пересекают «кадр захвата», а какие нам по-прежнему не нужно учитывать. Чтобы понять, как это обнаружить, давайте немного углубимся в анатомию нашей функции обратного вызова и посмотрим, из чего состоят такие записи.

Обратный вызов IntersectionObserver

Во-первых, функция обратного вызова для IntersectionObserver принимает два аргумента, и мы будем говорить о них в обратном порядке, начиная со второго аргумента. Наряду с вышеупомянутым Array наблюдаемых записей, пересекающих наш «кадр захвата», функция обратного вызова получает в качестве второго аргумента самого наблюдателя .

Ссылка на сам наблюдатель

 new IntersectionObserver(function(entries, SELF) {…});

Получение ссылки на сам наблюдатель полезно во многих сценариях, когда вы хотите прекратить наблюдение за каким-либо элементом после того, как он был обнаружен IntersectionObserver в первый раз. Такие сценарии, как отложенная загрузка изображений, отложенная загрузка других активов и т. д. Если вы хотите прекратить наблюдение за элементом, IntersectionObserver предоставляет метод unobserve(element-to-stop-observing) , который можно запустить в функции обратного вызова после выполнения некоторых действий над наблюдаемым элементом (например, фактическая отложенная загрузка изображения, например ).

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

IntersectionObserverEntry

 new IntersectionObserver(function(ENTRIES, self) {…});

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

Во-первых, записи типа IntersectionObserverEntry содержат информацию о трех разных прямоугольниках — определяющих координаты и границы элементов, участвующих в процессе:

  • rootBounds : прямоугольник для «захвата кадра» ( root + rootMargin );
  • boundingClientRect : прямоугольник для самого наблюдаемого элемента;
  • intersectionRect : область «кадра захвата», пересекаемая наблюдаемым элементом.
Прямоугольники IntersectionObserverEntry
Все ограничивающие прямоугольники, задействованные в IntersectionObserverEntry, рассчитываются за вас.

Действительно крутая вещь в том, что эти прямоугольники вычисляются для нас асинхронно, заключается в том, что они дают нам важную информацию, связанную с позиционированием элемента, без вызова нами getBoundingClientRect() , offsetTop , offsetLeft и других дорогостоящих свойств и методов позиционирования, вызывающих перебор макета. Чистая победа за производительность!

Еще одно интересное для нас свойство интерфейса IntersectionObserverEntryisIntersecting . Это свойство удобства указывает, пересекает ли наблюдаемый элемент в данный момент «кадр захвата» или нет. Мы могли бы, конечно, получить эту информацию, посмотрев на intersectionRect (если этот прямоугольник не 0×0, элемент пересекает «кадр захвата»), но предварительно вычислить его для нас довольно удобно.

isIntersecting можно использовать, чтобы узнать, входит ли наблюдаемый элемент в «кадр захвата» или уже покидает его. Чтобы выяснить это, сохраните значение этого свойства как глобальный флаг, и когда новая запись для этого элемента поступит в вашу функцию обратного вызова, сравните его новый isIntersecting с этим глобальным флагом:

  • Если было false , а теперь стало true , то элемент попадает в «кадр захвата»;
  • Если все наоборот и сейчас оно false , хотя раньше было true , то элемент покидает «кадр захвата».

isIntersecting — это именно то свойство, которое помогает нам решить проблему, которую мы обсуждали ранее, т. е. отделить записи для элементов, которые действительно пересекают «кадр захвата», от шума тех, которые являются просто инициализацией записи.

 let isLeaving = false; let observer = new IntersectionObserver(function(entries) { entries.forEach(entry => { if (entry.isIntersecting) { // we are ENTERING the "capturing frame". Set the flag. isLeaving = true; // Do something with entering entry } else if (isLeaving) { // we are EXITING the "capturing frame" isLeaving = false; // Do something with exiting entry } }); }, config);

ПРИМЕЧАНИЕ . В Microsoft Edge 15 свойство isIntersecting не было реализовано, возвращая undefined значение, несмотря на полную поддержку IntersectionObserver в противном случае. Это было исправлено в июле 2017 года и доступно с Edge 16.

Интерфейс IntersectionObserverEntry предоставляет еще одно предварительно рассчитанное свойство удобства: intersectionRatio . Этот параметр можно использовать для тех же целей, что и isIntersecting , но он обеспечивает более детальное управление, поскольку представляет собой число с плавающей запятой, а не логическое значение. Значение intersectionRatio указывает, какая часть области наблюдаемого элемента пересекает «кадр захвата» (отношение площади intersectionRect к площади boundingClientRect ). Опять же, мы могли бы сделать этот расчет сами, используя информацию из этих прямоугольников, но хорошо, что это сделали за нас.

Разве это не выглядит уже знакомым? Да, свойство <code>intersectionRatio</code> похоже на свойство <code>threshold</code> конфигурации Observer. Разница в том, что последний определяет <em>когда</em> запускать Observer, а первый указывает реальную ситуацию пересечения (которая немного отличается от <code>threshold</code> из-за асинхронной природы Observer).
Разве это не выглядит уже знакомым? Да, свойство intersectionRatio аналогично threshold свойству конфигурации Observer. Разница в том, что последний определяет *когда* запускать Observer, а первый указывает реальную ситуацию пересечения (которая немного отличается от threshold из-за асинхронной природы Observer).

target — еще одно свойство интерфейса IntersectionObserverEntry , к которому вам может понадобиться обращаться довольно часто. Но здесь нет абсолютно никакой магии — это просто исходный элемент, который был передан в observe() вашего Observer. Так же, как и event.target , к которому вы привыкли при работе с событиями.

Чтобы получить полный список свойств интерфейса IntersectionObserverEntry , проверьте спецификацию.

Возможные применения

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

Отложенная функциональность

Прежде всего, рассмотрим пример, раскрывающий основные принципы, лежащие в основе идеи IntersectionObserver . Допустим, у вас есть элемент, который должен выполнять множество вычислений после того, как он появится на экране. Например, ваше объявление должно регистрировать просмотр только тогда, когда оно действительно было показано пользователю. Но теперь давайте представим, что у вас есть автоматически воспроизводимый элемент карусели где-то под первым экраном на вашей странице.

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

Запуск карусели, вообще, тяжелая задача. Обычно это включает в себя таймеры JavaScript, вычисления для автоматической прокрутки элементов и т. д. Все эти задачи нагружают основной поток, и когда они выполняются в режиме автоматического воспроизведения, нам трудно узнать, когда наш основной поток получает этот удар. Когда мы говорим о приоритезации контента на нашем первом экране и хотим как можно скорее использовать First Meaningful Paint и Time To Interactive, заблокированный основной поток становится узким местом для нашей производительности.

Чтобы решить эту проблему, мы можем отложить воспроизведение такой карусели до тех пор, пока она не попадет в область просмотра браузера. В этом случае мы будем использовать наши знания и пример для параметра isIntersecting интерфейса IntersectionObserverEntry .

 const carousel = document.getElementById('carousel'); let isLeaving = false; let observer = new IntersectionObserver(function(entries) { entries.forEach(entry => { if (entry.isIntersecting) { isLeaving = true; entry.target.startCarousel(); } else if (isLeaving) { isLeaving = false; entry.target.stopCarousel(); } }); } observer.observe(carousel);

Здесь мы воспроизводим карусель только тогда, когда она попадает в наше окно просмотра. Обратите внимание на отсутствие объекта config , переданного при инициализации IntersectionObserver : это означает, что мы полагаемся на параметры конфигурации по умолчанию. Когда карусель выходит из нашего окна просмотра, мы должны остановить ее воспроизведение, чтобы не тратить ресурсы на более неважные элементы.

Ленивая загрузка ресурсов

Это, пожалуй, самый очевидный вариант использования IntersectionObserver : мы не хотим тратить ресурсы на загрузку того, что пользователю сейчас не нужно. Это даст огромную пользу вашим пользователям: пользователям не нужно будет скачивать, а их мобильным устройствам не нужно будет парсить и компилировать массу бесполезной информации, которая им не нужна в данный момент. Неудивительно, что это также повысит производительность вашего приложения.

Ленивая загрузка изображений ниже сгиба
Ленивая загрузка ресурсов, таких как изображения, расположенные под первым экраном — наиболее очевидное применение IntersectionObserver.

Раньше, чтобы отложить загрузку и обработку ресурсов до того момента, когда пользователь сможет вывести их на экран, мы имели дело с прослушивателями событий на таких событиях, как scroll . Проблема очевидна: это слишком часто раздражало слушателей. Поэтому нам пришлось придумать идею регулирования или устранения дребезга выполнения обратного вызова. Но все это добавило большого давления на наш основной поток, блокируя его, когда мы больше всего в нем нуждались.

Итак, возвращаясь к IntersectionObserver в сценарии с ленивой загрузкой, на что мы должны обратить внимание? Давайте рассмотрим простой пример отложенной загрузки изображений.

См. загрузку Pen Lazy в IntersectionObserver от Дениса Мишунова (@mishunov) на CodePen.

См. загрузку Pen Lazy в IntersectionObserver от Дениса Мишунова (@mishunov) на CodePen.

Попробуйте медленно прокрутить эту страницу до «третьего экрана» и посмотрите на окно мониторинга в правом верхнем углу: оно сообщит вам, сколько изображений уже загружено.

В основе HTML-разметки для этой задачи лежит простая последовательность изображений:

 … <img data-src="https://blah-blah.com/foo.jpg"> …

Как видите, изображения должны приходить без тегов src : как только браузер увидит атрибут src , он сразу же начнет скачивать это изображение, что противоречит нашим намерениям. Следовательно, мы не должны помещать этот атрибут в наши изображения в HTML, и вместо этого мы можем полагаться на некоторый атрибут data- , такой как data-src здесь.

Другая часть этого решения — это, конечно же, JavaScript. Давайте сосредоточимся на основных битах здесь:

 const images = document.querySelectorAll('[data-src]'); const config = { … }; let observer = new IntersectionObserver(function (entries, self) { entries.forEach(entry => { if (entry.isIntersecting) { … } }); }, config); images.forEach(image => { observer.observe(image); });

Что касается структуры, здесь нет ничего нового: все это мы рассмотрели ранее:

  • Мы получаем все сообщения с нашими атрибутами data-src ;
  • Установите config : для этого сценария вы хотите расширить свой «кадр захвата», чтобы обнаруживать элементы немного ниже нижней части области просмотра;
  • Зарегистрируйте IntersectionObserver с этой конфигурацией;
  • Перебираем наши изображения и добавляем все из них для наблюдения этим IntersectionObserver ;

Самое интересное происходит внутри функции обратного вызова, вызываемой для записей. Там три основных шага.

  1. Прежде всего, мы обрабатываем только те элементы, которые действительно пересекают нашу «рамку захвата». Этот фрагмент уже должен быть вам знаком.

     entries.forEach(entry => { if (entry.isIntersecting) { … } });

  2. Затем мы каким-то образом обрабатываем запись, конвертируя наше изображение с data-src в настоящий <img src="…"> .

     if (entry.isIntersecting) { preloadImage(entry.target); … }
    Это заставит браузер окончательно загрузить изображение. preloadImage() — очень простая функция, о которой здесь не стоит упоминать. Просто прочитайте источник.

  3. Следующий и последний шаг: так как ленивая загрузка — это разовое действие и нам не нужно загружать изображение каждый раз, когда элемент попадает в наш «кадр захвата», мы должны не unobserve за уже обработанным изображением. Так же, как мы должны делать это с element.removeEventListener() для наших обычных событий, когда они больше не нужны, чтобы предотвратить утечку памяти в нашем коде.

     if (entry.isIntersecting) { preloadImage(entry.target); // Observer has been passed as self to our callback self.unobserve(entry.target); }

Примечание. Вместо unobserve(event.target) мы могли бы также вызвать disconnect() : он полностью отключит наш IntersectionObserver и больше не будет наблюдать за изображениями. Это полезно, если единственное, о чем вы заботитесь, — это первое попадание вашего наблюдателя. В нашем случае нам нужно, чтобы Observer продолжал следить за изображениями, поэтому пока не следует отключаться.

Не стесняйтесь разветвлять пример и играть с различными настройками и опциями. Однако есть одна интересная вещь, о которой стоит упомянуть , когда вы хотите, в частности, лениво загружать изображения. Вы всегда должны помнить о поле, генерируемом наблюдаемым элементом! Если вы посмотрите пример, то заметите, что CSS для изображений в строках 41–47 содержит якобы избыточные стили, в т.ч. min-height: 100px . Это делается для того, чтобы придать заполнителям изображения ( <img> без атрибута src ) некоторый вертикальный размер. Зачем?

  • Без вертикальных размеров все теги <img> генерировали бы поле 0×0;
  • Поскольку <img> по умолчанию создает своего рода inline-block , все эти блоки 0 × 0 будут выровнены бок о бок на одной строке;
  • Это означает, что ваш IntersectionObserver будет регистрировать все (или, в зависимости от скорости прокрутки, почти все) изображения одновременно — возможно, это не совсем то, чего вы хотите достичь.

Подсветка текущего раздела

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

См. Текущий раздел Pen Highlighting в IntersectionObserver Дениса Мишунова (@mishunov) на CodePen.

См. Текущий раздел Pen Highlighting в IntersectionObserver Дениса Мишунова (@mishunov) на CodePen.

Структурно он похож на пример для отложенной загрузки изображений и имеет ту же базовую структуру со следующими исключениями:

  • Теперь мы хотим наблюдать не изображения, а разделы на странице;
  • Достаточно очевидно, что у нас также есть другая функция для обработки записей в нашем обратном вызове ( intersectionHandler(entry) ). Но это не интересно: все, что он делает, это переключает класс CSS.

Что здесь интересно, так это объект config :

 const config = { rootMargin: '-50px 0px -55% 0px' };

Вы спросите, почему не значение по умолчанию 0px для rootMargin ? Ну, просто потому, что выделение текущего раздела и отложенная загрузка изображения совершенно разные в том, чего мы пытаемся достичь. При ленивой загрузке мы хотим начать загрузку до того, как изображение попадет в представление. Поэтому для этой цели мы расширили нашу «рамку захвата» на 50 пикселей внизу. Напротив, когда мы хотим выделить текущий раздел, мы должны быть уверены, что этот раздел действительно виден на экране. И не только это: мы должны быть уверены, что пользователь действительно читает или собирается читать именно этот раздел. Следовательно, мы хотим, чтобы секция выходила чуть больше чем на половину области просмотра снизу, прежде чем мы сможем объявить ее активной. Кроме того, мы хотим учитывать высоту панели навигации, поэтому мы удаляем высоту панели из «кадра захвата».

Захват кадра для текущего раздела
Мы хотим, чтобы наблюдатель обнаруживал только те элементы, которые попадают в «кадр захвата» между 50 пикселями сверху и 55% области просмотра снизу.

Также обратите внимание , что в случае выделения текущего элемента навигации мы не хотим прекращать наблюдение за чем-либо. Здесь мы всегда должны контролировать IntersectionObserver , поэтому вы не найдете здесь ни disconnect() , ни unobserve() .

Резюме

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

Почему IntersectionObserver полезен для вас?

  • IntersectionObserver — это асинхронный неблокирующий API!
  • IntersectionObserver заменяет наши дорогие слушатели на события scroll или resize .
  • IntersectionObserver выполняет все дорогостоящие вычисления, такие как getClientBoundingRect() , за вас, так что вам это не нужно.
  • IntersectionObserver следует структурному шаблону других наблюдателей, поэтому теоретически его должно быть легко понять, если вы знакомы с тем, как работают другие наблюдатели.

О чем следует помнить

Если мы сравним возможности IntersectionObserver с миром window.addEventListener('scroll') откуда все это взялось, то в этом Observer трудно будет увидеть какие-то минусы. Итак, давайте просто отметим некоторые вещи, которые следует иметь в виду:

  • Да, IntersectionObserver — это асинхронный неблокирующий API. Это здорово знать! Но еще важнее понимать, что код, который вы запускаете в своих обратных вызовах, не будет выполняться асинхронно по умолчанию, даже если сам API является асинхронным. Таким образом, все еще есть шанс устранить все преимущества, которые вы получаете от IntersectionObserver , если вычисления вашей функции обратного вызова делают основной поток невосприимчивым. Но это другая история.
  • Если вы используете IntersectionObserver для ленивой загрузки ресурсов (например, изображений), запустите .unobserve(asset) после загрузки ресурса.
  • IntersectionObserver может обнаруживать пересечения только для элементов, которые появляются в структуре форматирования документа. Чтобы было понятно: наблюдаемые элементы должны генерировать блок и каким-то образом влиять на макет. Вот лишь несколько примеров, чтобы дать вам лучшее понимание:

    • Элементы с display: none из них не может быть и речи;
    • opacity: 0 или visibility:hidden создайте поле (даже невидимое), чтобы оно было обнаружено;
    • Абсолютно позиционированные элементы width:0px; height:0px width:0px; height:0px в порядке. Though, it has to be noted that absolutely positioned elements fully positioned outside of parent's borders (with negative margins or negative top , left , etc.) and are cut out by parent's overflow: hidden won't be detected: their box is out of scope for the formatting structure.
IntersectionObserver: Now You See Me
IntersectionObserver: Now You See Me

I know it was a long article, but if you're still around, here are some links for you to get an even better understanding and different perspectives on the Intersection Observer API:

  • Intersection Observer API on MDN;
  • IntersectionObserver polyfill;
  • IntersectionObserver polyfill as npm module;
  • Lazy-Loading Images with IntersectionObserver [video] by amazing Paul Lewis;
  • Basic and short (just 01:39), but very informative introduction to IntersectionObserver [video] by Surma.

With this, I would like to make a pause in our discussion to give you an opportunity to play with this technology and realize all of its convenience. So, go play with it. The article is finally over. This time I really mean it.