信頼は良い、モニタリングはよりよい: 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(DocumentOrShadowRoot.elementFromPoint() など)で DOM を分析し、さらに深く掘り下げることで確認できます。 一方、問題の要素がサードパーティの 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 によるものです。