信頼は良い、モニタリングはよりよい: 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 の場合、実装ではそのことが保証されません。

specの重要な点は、実装で偽陰性の報告が許可されている(つまり、ターゲット要素が完全に表示されて変更されていない場合でも isVisiblefalse に設定する)ことです。パフォーマンスやその他の理由から、ブラウザでは、境界ボックスと直線ジオメトリのみに制限され、border-radius のような変更ではピクセル完璧な結果が得られません。

とはいえ、どのような状況でも誤検出許可されません(つまり、ターゲット要素が完全に表示されず、変更されていない場合には、isVisibletrue に設定します)。

新しいコードが実際にどのようなものか

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

ここで注意すべき重要な点は、trackVisibilitytrue の場合、delay は少なくとも 100 である(つまり、100 ミリ秒ごとに 1 つの通知を超えないようにする)必要があります。前述のように、可視性の計算には費用がかかるため、この要件はパフォーマンスの低下とバッテリー消費の防止策です。デベロッパーは、遅延に最大許容値を使用します。

現在のspecでは、可視性は次のように計算されます。

  • オブザーバーの 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 BynensStefan Zager に同様に Chrome での機能のレビューと実装に感謝します。ヒーロー画像(作成者: Sergey Semin、Unsplash