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

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

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

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

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

image

Леди и джентельмены, поговорим об Intersection Observer API. Но перед тем, как мы начнем, давайте посмотрим на современные инструменты, которые привели нас к IntersectionObserver.

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


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

image
IntersectionObserver и PerformanceObserver — новые члены семьи обсерверов

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

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

Observer vs Event


«Observer», как подсказывает название, предназначен для наблюдения за происходящим в контексте страницы. Обсерверы могут следить за чем-либо происходящим на странице, вроде изменений в DOM. Они могут и следить за событиями жизненного цикла страницы. Обсерверы также могут запускать какие-либо функции обратного вызова. Внимательный читатель может незамедлительно обнаружить тут проблему и поинтересоваться: «А в чем, собственно, смысл? У нас же есть события, ровно для тех же целей?». Очень хорошая мысль! Посмотрим поближе и разберемся в этом.

image
Observer vs Event: в чем разница?

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

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

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

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

image
Обсерверы не собираются заменить события: они прекрасно уживаются вместе

Структура обсервера в целом


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

/**
* 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 — это массив значений, а не единичное значение.

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

Завершим “общую” часть этой дискуссии и погрузимся глубже в тему сегодняшней статьи — IntersectionObserver.

Разбирая IntersectionObserver


image
Разбирая IntersectionObserver

В первую очередь, выясним что же такое IntersectionObserver.

Согласно MDN:

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

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


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

  • root: Это корневой элемент, используемый для наблюдения. Он определяет базовую “область захвата” для наблюдаемых элементов. По умолчанию, root — это viewport вашего браузера, но на самом деле это может быть любой элемент в вашем DOM(в этом случае вы устанавливаете root как что-то вроде document.getElementById('your-element')). Имейте в виду, что в этом случае элементы, за которыми вы хотите наблюдать, должны находиться внутри DOM-дерева root-элемента.

    image
  • rootMargin: устанавливает отступ вокруг вашего root-элемента, который расширяет или сжимает “область захвата”, когда размеры вашего root-элемента не дают необходимой гибкости. Возможные варианты для этих конфигурационных значений похожи на значения margin в CSS, такие как rootMargin: '50px 20px 10px 40px'(верхний, правый, нижний левый). Можно использовать краткую форму записи(типа rootMargin: '50px') и выражать значения в px или %. По умолчанию rootMargin: '0px'.

    image
  • threshold: Не всегда желательно реагировать на пересечение наблюдаемым элементом границы “области захвата”(которая определяется комбинацией значений root и rootMargin) моментально. threshold задает процентное значение от такого пересечения, на которое обсервер должен реагировать, Оно может быть задано как единичное значение или как массив значений. Чтобы лучше понять, какой эффект производит threshold(я понимаю, что это иногда может сбивать с толку), посмотрим на несколько примеров:

    • threshold: 0: IntersectionObserver с этим значением по-умолчанию должен реагировать, когда самый первый пиксель наблюдаемого элемента пересечет одну из границ “области захвата”. Заметьте! IntersectionObserver отреагирует на оба варианта: a) когда элемент входит и b) когда элемент покидает “область захвата”
    • threshold: 0.5: Обсервер должен сработать, когда 50% от наблюдаемого элемента пересекает “область захвата”
    • threshold: [0, 0.2, 0.5, 1]: Обсервер должен реагировать в четырех случаях:

      • Самый первый пиксель наблюдаемого элемента входит в «область захвата»: элемент на самом деле все еще не внутри этой области, либо самый последний пиксель покидает “область захвата”: элемент уже не внутри этой области;
      • 20% элемента внутри “области захвата”(еще раз, направление не имеет значения для IntersectionObserver);
      • 50% элемента внутри “области захвата”;
      • 100% элемента внутри “области захвата”. Это прямо противоположно значению threshold: 0

image

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

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);
});

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

image
IntersectionObserver будет вызван для всех наблюдаемых элементов в момент их регистрации, но это не значит, что все они пересекают нашу «область захвата»

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

IntersectionObserver callback


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

Ссылка на сам обсервер


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

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

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

IntersectionObserverEntry


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

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

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

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

image
Все прямоугольники, доступные IntersectionObserverEntry заранее вычислены для вас

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

Другое свойство интерфейса IntersectionObserverEntry, которое нас интересует — это isIntersecting. Это удобное свойство, показывающее пересекает ли наблюдаемый элемент “область захвата” в данный момент или нет. Конечно, мы могли бы получить эту информацию с помощью intersectionRect(если прямоугольник не 0x0, значит элемент пересекает “область захвата”), но иметь такое свойство вычисленным заранее очень удобно.

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

  • Если оно было false, а теперь true значит элемент входит в “область захвата”
  • Если же наоборот, теперь оно false, но было true до этого, значит элемент покидает “область захвата”

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

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). Опять же, мы могли бы и сами посчитать это, используя информацию о наших прямоугольниках, но то, что это сделано за нас — просто отлично.

image
Кажется похожим на что-то? Да, intersectionRatio похоже на threshold объекта конфигурации обсервера. Разница в том, что последний определяет, когда вызвать обсервер, в то время как второй срабатывает при реальном пересечении(это слегка отличается от threshold из-за асинхронной природы обсерверов).

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

Чтобы ознакомиться с полным списком свойств, доступных для IntersectionObserverEntry, обратитесь к спецификации.

Возможные области применения


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

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


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

image
Когда у нас есть карусель или другая тяжелая функциональность за границей нашего приложения, его преждевременная загрузка — это пустая трата ресурсов

Выполнение карусели, в общем случае, задача тяжелая. Обычно сюда вовлечены джаваскриптовые таймеры, вычисления для автоматической прокрутки элементов и т.д. Все эти задачи нагружают основной поток, и когда все это делается в режиме auto-play, сложно понять, когда наш основной поток примет этот удар.Если мы говорим о приоритизации контента на нашем первом экране и хотим получить 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: мы не хотим тратить ресурсы, чтобы скачать что-то, что не нужно пользователю прямо сейчас. Это даст большое преимущество вашим пользователям: пользователям не нужно будет скачивать и их мобильные устройствам не нужно будет парсить и компилировать кучу информации, которая им в данный момент не нужна. Неудивительно, что это также улучшит производительность вашего приложения.

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

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

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

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

В основе 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;

Интересное происходит в колбеке, вызываемом в экземплярах IntersectionObserverEntry. Здесь три важных шага.

  1. Сначала мы обрабатываем только элементы, действительно пересекающие нашу “область захвата”. Этот код должен быть уже знаком вам:

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

  2. Затем мы каким-то образом обрабатываем IntersectionObserverEntry, конвертируя наше изображение с data-src в настоящее изображение вида .

    if (entry.isIntersecting) {
      preloadImage(entry.target);
      …
    }

    Это даст браузеру команду скачать изображение. preloadImage() — это очень простая функция, о которой тут можно не упоминать. Просто прочитайте исходники.
  3. Следующий и последний шаг: так как ленивая загрузка — действие одноразовое, и нам не нужно скачивать изображение каждый раз, когда элемент попадает в “область захвата”, нам следует вызвать unobserve() для уже обработанного изображения. Таким же образом, как мы вызываем 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 и отменит отслеживание изображений насовсем. Это полезно, если все что вам нужно — это отследить первое срабатывание вашего обсервера. В нашем случае нужно, чтобы обсервер продолжал следить за изображениями.

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

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

Подсветка текущей секции


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

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

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

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

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

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

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

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

Итог


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

Почему вам понравится IntersectionObserver?


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

Кое что на заметку


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

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

    • Об элементах с display: none не может быть и речи;
    • opacity: 0 или visibility: hidden создают блок(даже если и не видны), поэтому они детектируются;
    • Абсолютно спозиционированные элементы с width: 0; height: 0; подойдут. Хотя нужно заметить, что абсолютно спозиционированные элементы, спозиционированные за пределами границ элемента-родителя(с отрицательными значения margin, top, left и т.д.) и обрезанные родителями с помощью overflow: hidden; не будут задетектированы: их блоки за пределами структуры форматирования документа.

image
IntersectionObserver: теперь вы меня поняли

Я знаю, что статья была длинной, но если вы все еще здесь, вот несколько ссылок, которые дадут вам даже большее понимание и различные перспективы Intersection Observer API.


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

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


  1. Ashot
    01.02.2018 01:02
    +1

    У гугла есть ещё такая небольшая статья. Таких наглядных практических примеров там нет, это скорее краткий рассказ об IntersectionObserver (что умеет и как им пользоваться), но очень хорошая отправная точка. Я именно из неё узнал о его существовании.


  1. vasIvas
    01.02.2018 10:58

    Observer vs Event

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


  1. evgenWebm
    01.02.2018 16:01

    О, то что доктор прописал.
    Я как раз задумался о ленивой загрузке изображений в таблице.
    Спасибо, пошел пробовать.