信頼は重要だが、観察が最も重要: Intersection Observer v2

Browser Support

  • Chrome: 51.
  • Edge: 15.
  • Firefox: 55.
  • Safari: 12.1.

Source

Intersection Observer は、おそらく誰からも愛される API の 1 つで、すべての主要なブラウザで使用できます。デベロッパーは、画像や動画の遅延読み込み要素が position: sticky に達したときの通知分析イベントのトリガーなど、さまざまなユースケースでこの API を使用しています。

最も基本的な形式では、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 を使用すると、要素がウィンドウのビューポートにスクロールされたタイミングを把握できます。ただし、その要素が他のページ コンテンツによって覆われているか(オクルージョン)、transformopacityfilter などの CSS によって要素が変更され、要素が非表示になっているかどうかを判断することはできません。

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

可視性が重要な理由

残念ながら、インターネットには悪意のあるユーザーが存在します。たとえば、不正なサイト運営者がウェブサイトでクリック単価制広告を使用する可能性があります。少なくとも広告ネットワークが不正行為を発見するまでは、ユーザーをだまして広告をクリックさせ、収益を増やそうとする可能性があります。通常、このような広告は iframe で配信されます。

ユーザーをだますために、パブリッシャーは CSS を使用して広告の iframe を完全に透明にすることができます(iframe { opacity: 0; })。そして、ユーザーがクリックしたくなるような魅力的なコンテンツ(可愛い猫の動画など)の上に、透明な iframe を配置します。これはクリックジャッキングと呼ばれます。

クリックジャッキング攻撃の例については、デモの上部をご覧ください。猫の動画を「視聴」して、トリックモードを有効にしてみます。iframe 内の広告は、iframe が透明な状態で(意図せずに)クリックされた場合でも、クリックが正当なものとして登録されます。

広告を透明にして魅力的なものの上に重ね、ユーザーをだまして広告をクリックさせる。

Intersection Observer v2 の改善

Intersection Observer v2 では、人間が定義する要素の「可視性」を追跡できます。IntersectionObserver コンストラクタでオプションを設定すると、結果の IntersectionObserverEntry インスタンスに isVisible という新しいブール値フィールドが含まれます。isVisibletrue の場合、ブラウザは、要素が他のコンテンツによって完全に覆われておらず、表示を隠したり変更したりする視覚効果がないことを確認します。isVisiblefalse の場合、ブラウザはその保証を行うことはできません。

spec では偽陰性が許容されます。要素が実際に表示されていて変更されていない場合でも、isVisiblefalse になることがあります。パフォーマンスを向上させるため、ブラウザでは境界ボックスや長方形などの簡単な計算が使用され、border-radius などの複雑な詳細についてはすべてのピクセルがチェックされません。

ただし、誤検出はいかなる状況でも許可されません。つまり、要素が完全に表示されていて、変更されていない場合、isVisibletrue になりません。

これらの変更を適用する

IntersectionObserver コンストラクタは、次の 2 つの構成プロパティを追加で受け取るようになりました。

  • delay は、特定のターゲットについて、オブザーバーからの通知間の最小遅延時間(ミリ秒単位)を示す数値です。
  • trackVisibility は、オブザーバーがターゲットの可視性の変化を追跡するかどうかを示すブール値です。

trackVisibilitytrue の場合、delay100 以上の値に設定する必要があります(つまり、100 ミリ秒ごとに 1 回以下の通知)。可視性の計算はコストが高いため、パフォーマンスの低下とバッテリー消費量を抑えるための予防措置です。責任あるデベロッパーは、遅延の許容可能な最大値を使用する必要があります。

仕様では、可視性を計算します。バージョン 1 と同様に、オブザーバーの trackVisibility 属性が false の場合、ターゲットは表示されていると見なされます。

バージョン 2 では、ターゲットは次の場合に非表示とみなされます。

  • 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 v2, fallback 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 に感謝します。