Доверие — это хорошо, наблюдение — лучше: Intersection Observer v2

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

Intersection Observer v1 — один из тех API, который, вероятно, всеми любим, и теперь, когда Safari также поддерживает его , он наконец-то стал универсально использоваться во всех основных браузерах. Для быстрого ознакомления с API я рекомендую посмотреть Supercharged Microtip от Surma на Intersection Observer v1, который встроен ниже. Вы также можете прочитать подробную статью Сурма. Люди использовали Intersection Observer v1 для широкого спектра случаев использования, таких как отложенная загрузка изображений и видео , получение уведомлений, когда элементы достигают position: sticky , запуск аналитических событий и многое другое.

Для получения полной информации ознакомьтесь с документацией Intersection Observer на MDN , но в качестве краткого напоминания: вот как выглядит API Intersection Observer v1 в самом простом случае:

const onIntersection = (entries) => {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      console.log(entry);
    }
  }
};

const observer = new IntersectionObserver(onIntersection);
observer.observe(document.querySelector('#some-target'));

Что сложного в Intersection Observer v1?

Чтобы внести ясность, Intersection Observer v1 великолепен, но не идеален. Есть некоторые крайние случаи, когда API не справляется. Давайте посмотрим поближе! API Intersection Observer v1 может сообщить вам, когда элемент прокручивается в область просмотра окна, но он не сообщает вам, покрыт ли элемент каким-либо другим содержимым страницы (то есть, когда элемент перекрыт) или визуальный элемент элемента закрыт. display был изменен визуальными эффектами, такими как transform , opacity , filter и т. д., которые фактически могут сделать его невидимым.

Для элемента в документе верхнего уровня эту информацию можно определить, проанализировав DOM с помощью JavaScript, например, с помощью DocumentOrShadowRoot.elementFromPoint() , а затем копнув глубже. Напротив, ту же информацию невозможно получить, если рассматриваемый элемент находится в стороннем iframe.

Почему фактическая видимость так важна?

К сожалению, Интернет — это место, которое привлекает плохих людей с худшими намерениями. Например, сомнительный издатель, который размещает рекламу с оплатой за клик на содержательном сайте, может быть заинтересован в том, чтобы обманом заставить людей нажимать на их объявления, чтобы увеличить рекламные выплаты издателя (по крайней мере, на короткий период, пока рекламная сеть не поймает их). Обычно такие объявления показываются в iframe. Теперь, если издатель хотел, чтобы пользователи нажимали на такие объявления, он мог сделать рекламные iframe полностью прозрачными, применив правило CSS iframe { opacity: 0; } и наложение iframe поверх чего-то привлекательного, например, видео с милым котиком, на которое пользователи действительно захотят нажать. Это называется кликджекинг . Вы можете увидеть такую ​​атаку кликджекинга в действии в верхней части этой демонстрации (попробуйте «посмотреть» видео с котом и активировать «режим трюка»). Вы заметите, что объявление в iframe «думает», что оно получило законные клики, даже если оно было полностью прозрачным, когда вы (притворно непроизвольно) нажали на него.

Обманом заставить пользователя нажать на объявление, сделав его прозрачным и наложив поверх чего-то привлекательного.

Как Intersection Observer v2 исправляет это?

Intersection Observer v2 представляет концепцию отслеживания фактической «видимости» целевого элемента, как его определил бы человек. Если установить опцию в конструкторе IntersectionObserver , пересекающиеся экземпляры IntersectionObserverEntry будут содержать новое логическое поле с именем isVisible . true значение isVisible — это надежная гарантия базовой реализации того, что целевой элемент полностью не перекрыт другим содержимым и к нему не применяются визуальные эффекты, которые могли бы изменить или исказить его отображение на экране. Напротив, false значение означает, что реализация не может обеспечить такую ​​гарантию.

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

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

Как новый кодекс выглядит на практике?

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

Здесь важно отметить, что когда trackVisibility имеет true , delay должна быть не менее 100 (то есть не более одного уведомления каждые 100 мс). Как отмечалось ранее, расчет видимости требует больших затрат, и это требование является мерой предосторожности против снижения производительности и расхода заряда батареи. Ответственный разработчик будет использовать максимально допустимое значение задержки.

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

  • Если атрибут trackVisibility наблюдателя имеет false , то цель считается видимой. Это соответствует текущему поведению версии v1.

  • Если цель имеет эффективную матрицу преобразования, отличную от 2D-переноса или пропорционального 2D-масштабирования, то цель считается невидимой.

  • Если цель или любой элемент в содержащей ее цепочке блоков имеет эффективную непрозрачность, отличную от 1,0, то цель считается невидимой.

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

  • Если реализация не может гарантировать, что цель полностью не перекрыта другим содержимым страницы, то цель считается невидимой.

Это означает, что текущие реализации довольно консервативны и гарантируют видимость. Например, применение почти незаметного фильтра оттенков серого, такого как filter: grayscale(0.01%) , или установка почти невидимой прозрачности с opacity: 0.99 сделает элемент невидимым.

Ниже приведен пример короткого кода, иллюстрирующий новые функции API. Вы можете увидеть логику отслеживания кликов в действии во втором разделе демонстрации (а теперь попробуйте «посмотреть» видео со щенком). Обязательно снова активируйте «режим трюка», чтобы немедленно превратить себя в сомнительного издателя и увидеть, как Intersection Observer v2 предотвращает отслеживание незаконных кликов по рекламе. На этот раз Intersection Observer v2 нас поддержит! 🎉

Intersection Observer v2 предотвращает непреднамеренный клик по объявлению.

<!DOCTYPE html>
<!-- This is the ad running in the iframe -->
<button id="callToActionButton">Buy now!</button>
// This is code running in the iframe.

// The iframe must be visible for at least 800ms prior to an input event
// for the input event to be considered valid.
const minimumVisibleDuration = 800;

// Keep track of when the button transitioned to a visible state.
let visibleSince = 0;

const button = document.querySelector('#callToActionButton');
button.addEventListener('click', (event) => {
  if ((visibleSince > 0) &&
      (performance.now() - visibleSince >= minimumVisibleDuration)) {
    trackAdClick();
  } else {
    rejectAdClick();
  }
});

const observer = new IntersectionObserver((changes) => {
  for (const change of changes) {
    // ⚠️ Feature detection
    if (typeof change.isVisible === 'undefined') {
      // The browser doesn't support Intersection Observer v2, falling back to v1 behavior.
      change.isVisible = true;
    }
    if (change.isIntersecting && change.isVisible) {
      visibleSince = change.time;
    } else {
      visibleSince = 0;
    }
  }
}, {
  threshold: [1.0],
  // 🆕 Track the actual visibility of the element
  trackVisibility: true,
  // 🆕 Set a minimum delay between notifications
  delay: 100
}));

// Require that the entire iframe be visible.
observer.observe(document.querySelector('#ad'));

Благодарности

Спасибо Симеону Винсенту , Йоаву Вайсу и Матиасу Байненсу за рецензирование этой статьи, а также Стефану Загеру за рецензирование и реализацию этой функции в Chrome. Изображение героя Сергея Семина на Unsplash .