信頼は良い、モニタリングはよりよい: Intersection Observer v2

Intersection Observer v2 では、交差自体を検出するだけでなく、交差時に交差する要素が可視であったかどうかを検出する機能が追加されています。

Intersection Observer v1 は、おそらくすべての人に愛されている API の 1 つです。Safari でもサポートされるようになったため、すべての主要ブラウザで使用できるようになりました。API について簡単に復習するには、以下に埋め込まれている Intersection Observer v1 に関する SurmaSupercharged Microtip をご覧ください。Surma による詳細な記事もご覧ください。Intersection Observer v1 は、画像や動画の遅延読み込み要素が position: sticky に達したときに通知するアナリティクス イベントを発生させるなど、さまざまなユースケースで使用されています。

詳細については、MDN の Intersection Observer のドキュメントをご覧ください。最も基本的なケースでの Intersection Observer v1 API は次のようになります。

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 が不十分なコーナーケースもあります。詳しく見てみましょう。Intersection Observer v1 API は、要素がウィンドウのビューポートにスクロールされたタイミングを通知できますが、要素が他のページ コンテンツで覆われているかどうか(つまり、要素が遮蔽されているかどうか)、または要素の視覚的な表示が transformopacityfilter などの視覚効果によって変更され、実質的に要素が見えなくなっているかどうかは通知しません。

トップレベル ドキュメント内の要素の場合、この情報は JavaScript で DOM を分析し(DocumentOrShadowRoot.elementFromPoint() など)、さらに詳細を調べることで確認できます。一方、問題の要素がサードパーティの iframe 内にある場合、同じ情報を取得することはできません。

実際の視認性が重要である理由

残念ながら、インターネットは悪意のある行為者を引き付ける場所です。たとえば、コンテンツ サイトでクリック課金広告を配信している不正なパブリッシャーは、ユーザーを騙して広告をクリックさせ、パブリッシャーの広告収入を増やす(少なくとも広告ネットワークが不正行為に気付くまでの短い期間)インセンティブを受け取る可能性があります。通常、このような広告は iframe で配信されます。パブリッシャーがそのような広告をクリックしてもらえるようにするには、CSS ルール iframe { opacity: 0; } を適用して広告 iframe を完全に透明にし、ユーザーが実際にクリックしたくなるような魅力的なコンテンツ(かわいい猫の動画など)の上に iframe を重ねます。これは「クリックジャッキング」と呼ばれます。このようなクリックジャッキング攻撃の動作は、このデモの上部セクションで確認できます(猫の動画の「視聴」を試して「トリックモード」を有効にしてください)。iframe 内の広告は、(意図せず)クリックされたときに完全に透明だったとしても、正当なクリックが発生したと「認識」します。

広告を透明にして魅力的なコンテンツの上に重ねて表示し、ユーザーが広告をクリックするように誘導する。

Intersection Observer v2 ではどのように解決されるのですか?

Intersection Observer v2 では、人間が定義するように、ターゲット要素の実際の「可視性」をトラッキングするというコンセプトが導入されています。IntersectionObserver コンストラクタでオプションを設定すると、交差する IntersectionObserverEntry インスタンスに isVisible という名前の新しいブール型フィールドが含まれます。isVisibletrue 値は、ターゲット要素が他のコンテンツによって完全に遮られていない、および画面上の表示を変更または歪める視覚効果が適用されていないことを、基盤となる実装から強く保証します。一方、値が false の場合、実装でその保証を行うことはできません。

仕様の重要な詳細は、実装で偽陰性を報告することが許可されていることです(つまり、ターゲット要素が完全に可視で変更されていない場合でも、isVisiblefalse に設定します)。パフォーマンスなどの理由から、ブラウザは境界ボックスと直線ジオメトリの処理に限定されています。border-radius などの変更でピクセル単位の正確な結果を得ようとはしません。

ただし、誤検出はいかなる状況でも許可されません(つまり、ターゲット要素が完全に表示されておらず、変更されていない場合に isVisibletrue に設定することはできません)。

新しいコードの実際の例

IntersectionObserver コンストラクタに、delaytrackVisibility の 2 つの構成プロパティが追加されました。delay は、特定のターゲットに対するオブザーバーからの通知間の最小遅延(ミリ秒単位)を示す数値です。trackVisibility は、オブザーバーがターゲットの公開設定の変更を追跡するかどうかを示すブール値です。

ここで重要なのは、trackVisibilitytrue の場合、delay は少なくとも 100 にする必要があります(つまり、100 ミリ秒あたり 1 回以下の通知)。前述のとおり、可視性の計算は負荷が高いため、この要件はパフォーマンスの低下とバッテリーの消費を防ぐための予防措置です。担当デベロッパーは、遅延に許容できる最大値を使用します。

現在の仕様では、公開設定は次のように計算されます。

  • オブザーバーの trackVisibility 属性が false の場合、ターゲットは可視と見なされます。これは現在の v1 の動作に対応しています。

  • ターゲットに 2D 変換または比例 2D アップスケーリング以外の有効な変換行列がある場合、ターゲットは非表示と見なされます。

  • ターゲットまたはそのブロックチェーン内の要素の効果的な不透明度が 1.0 以外の場合、ターゲットは非表示と見なされます。

  • ターゲットまたはそのブロックチェーン内の要素にフィルタが適用されている場合、ターゲットは非表示と見なされます。

  • 実装で、ターゲットが他のページ コンテンツによって完全に遮られていない状態を保証できない場合、ターゲットは非表示と見なされます。

つまり、現在の実装では可視性を保証するためにかなり保守的な方法が取られています。たとえば、filter: grayscale(0.01%) のようなほとんど目立たないグレースケール フィルタを適用したり、opacity: 0.99 でほとんど目立たない透明度を設定したりすると、要素はすべて非表示になります。

次のコードサンプルは、新しい API 機能を示しています。クリック トラッキング ロジックの動作は、デモの第 2 セクションで確認できます(まずは、子犬の動画の「視聴」を試してみてください)。必ず「トリックモード」を再度有効にして、すぐに不正なパブリッシャーに変身し、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'));

謝辞

この記事の審査に協力してくれた Simeon VincentYoav WeissMathias Bynens の皆様、および Chrome への機能の実装と審査に協力してくれた Stefan Zager の皆様に感謝します。ヒーロー画像は、Unsplash の Sergey Semin によるものです。