「ナイスですね!」Intersection Observer!

Adways Advent Calendar 2019 7日目の記事です。

http://blog.engineer.adways.net/entry/advent_calendar_2019


 

はじめに

12月10日を担当します、井古田です。
最近はプロダクトマネージャー(PdM)として仕事をしています!
久しぶりの会社のブログ投稿なのでめっちゃ緊張していますが頑張りますー

概要

今回紹介するのはIntersection Observerです!
ネット広告業界では不正なインプレッション(※広告表示)などがあったりするので、そういった時の対策として役立てたりします。
また、広告を掲載してもらっているメディアさんのサイトにあまり負荷をかけないように調整することもできます。

Intersection Observerとは?

直訳すると「Intersection(要素間交差)」を「Observe(監視)」するAPIです。
任意の要素(DOM)同士の交差を監視することが出来ます。
デフォルトの設定ではviewport(見えている範囲)とある要素が交差したら何かするというものです。
つまり、表示領域(可視領域)の中にある要素が見えたらイベントを発火するイメージです。
従来のスクロールを利用したイベントではないので、スクロールのたびに呼ばれることもなくサイトのパフォーマンス面でも便利に使えます!

Intersection Observerの使い方

const opts = {
  root: null,
  rootMargin: "0px",
  threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
};

const io = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    console.log(entry.isIntersecting);
    console.log(entry.intersectionRatio);
    console.log(entry.target);
    let intersectionRatio = `${Math.round(entry.intersectionRatio*100)}%`;
    document.getElementById("ratio").innerText = intersectionRatio;
  });
}, opts);

let target = document.getElementById("target")
io.observe(target);

Intesection Observer APIの使い方は非常にシンプルです。
new IntersectionObserver(callback, options)
第1引数にコールバック関数を渡し、第2引数にオプション設定のオブジェクトを渡します。
IntersectionObserverオブジェクトが作成できたらio.observe(target)のように、交差監視したい要素をobserveするだけです。

  • callback(第1引数)

    • 監視対象の要素が下記optionsのroot要素(※デフォルトはviewport)と交差したタイミングでコールバック関数が呼ばれます
    • 例:img要素が画面の表示領域に入ったら画像を読み込むなどの遅延ロード
  • options(第2引数)

    • root
      • 交差監視をする枠の要素を指定できます
        • rootを指定することで、任意の要素との交差判定が可能です
      • デフォルトはviewportなので、見ている画面との交差監視になります
    • rootMargin
      • 交差を検知するroot要素からの距離を指定できます
      • ◯◯pxを指定すると、root要素からの指定した距離を踏まえて交差判定をすることができます
        • 要素が見える少し前にイベントを発火させることできます
        • マイナス値を設定して、要素がある程度見えてからイベントを発火させることも可能です
      • CSSのmarginみたいな指定ができます
        • rootMargin: "10px 0px 10px 0px"
    • threshold
      • 交差する割合(領域)が、指定した値になるたびにコールバック関数を呼ぶことができます
      • 0〜1の間を数値もしくは配列形式で入力します
        • 0のとき、要素がrootの領域と少しでも交差したらコールバック関数が呼ばれる
        • 1の場合、要素が完全にrootの領域に入ったことになります
      • デフォルトは0です
  • io.observe(element)

    • 指定した要素の監視をはじめる
  • io.unobserve(element)

    • 指定した要素の監視をやめる。1度限りの処理も対応できます。
  • io.disconnect()

    • 監視対象の全ての要素の監視を解除することができる

IntersectionObserverEntry

const io = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    console.log(entry.isIntersecting);
    console.log(entry.intersectionRatio);
    console.log(entry.target);
    let intersectionRatio = `${Math.round(entry.intersectionRatio*100)}%`;
    document.getElementById("ratio").innerText = intersectionRatio;
  });
}, opts);

コールバック関数の第1引数には、監視した要素の数だけのIntersectionObserverEntryオブジェクトが入ります。

  • isIntersecting
    • 監視対象の要素がroot要素(※デフォルトはviewport)と交差しているかどうかのブール値を返す
  • intersectionRatio
    • 交差している割合(領域)を0.0〜1.0の範囲で返す
  • target
    • root要素と交差した要素

その他プロパティはこちらから

デモ画像

画像の遅延ロードをやってみる

続いては、IntersectionObserver APIを利用して画像の遅延ロードを実装してみたいと思います。
完成形は以下↓のイメージです!

※遅延ロードを分かりやすくするために表示領域に入ってから画像を読み込んでいます

ソースコード

<body>
  <ul>
    <li><img src="" data-src="./img/img_1.jpg"/></li>
    <li><img src="" data-src="./img/img_2.jpg"/></li>
    <li><img src="" data-src="./img/img_3.jpg"/></li>
    <li><img src="" data-src="./img/img_4.jpg"/></li>
    <li><img src="" data-src="./img/img_5.jpg"/></li>
    <li><img src="" data-src="./img/img_6.jpg"/></li>
    <li><img src="" data-src="./img/img_7.jpg"/></li>
    <li><img src="" data-src="./img/img_8.jpg"/></li>
  </ul>
  <script src="./io.index.js" type="text/javascript"></script>
</body>
const opts = {
  root: null,
  rootMargin: "0px 0px -200px",
  threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
};

const io = new IntersectionObserver((entries, obj) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      let target = entry.target;
      target.src = target.dataset.src;
      obj.unobserve(target);
    }
  });
}, opts);

let targets = document.querySelectorAll("img[data-src]")
for(let i = 0, l = targets.length; l > i; i++) {
  io.observe(targets[i]);
}

内容はすごくシンプルです。
まず下記で監視する要素を取得し、observeしていきます。

let targets = document.querySelectorAll("img[data-src]")
for (let i = 0, l = targets.length; l > i; i++) {
  io.observe(targets[i]);
}

コールバック関数が呼ばれると、isIntersectingを利用して交差しているかの判定を行います。
交差していれば、imgタグのdata-srcの内容をsrcに入れています。
最後にunobserveを利用して監視をやめて、一度だけ実行されるようにします。

const io = new IntersectionObserver((entries, obj) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      let target = entry.target;
      target.src = target.dataset.src;
      obj.unobserve(target);
    }
  });
}, opts);

今回はオプションのrootMargin(rootMargin: "0px 0px -200px")を利用して、viewport(可視領域)に入ってからのイベント発火タイミングを調整しています。
もちろんintersectionRatioを利用して、交差している割合を評価しながらsrcの書き換えを行うことも可能です。

ビューアブルインプレッションを計測してみたい

続いては、ビューアブルインプレッションの計測をしていきたいと思います。
※ビューアブルインプレッションとは実際にユーザーが閲覧できる状態にあった広告インプレッションのことです。

完成形は以下↓のイメージです!

ソースコード

HTMLの部分には変更はありません。

const opts = {
  root: null,
  rootMargin: "0px",
  threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
};

const viewableThreshold = 70;

const loadImage = (target) => {
  target.src = target.dataset.src;
};

const isViewable = (intersectionRatio) => {
  let ratio = Math.round(intersectionRatio*100);
  console.log(`Ratio: ${ratio}%`);
  return (ratio >= viewableThreshold);
};

const viewableSend = () => {
  if (navigator.sendBeacon) {
    const url = 'https://xxxxx.xxxx';
    let fd = new FormData();
    fd.append('view_imp', 'xxxxxx');
    navigator.sendBeacon(url, fd);
  }
  else {
    console.log("navigator sendBeacon not supported");
  }
};

const io = new IntersectionObserver((entries, obj) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      loadImage(entry.target);
    }
    if (isViewable(entry.intersectionRatio)) {
      console.log('viewable')
      viewableSend();
      obj.unobserve(entry.target);
    }
  });
}, opts);

let targets = document.querySelectorAll("img[data-src]")
for (let i = 0, l = targets.length; l > i; i++) {
  io.observe(targets[i]);
}

※下記は適当な値を設定しているため、適宜修正してもらえればと思います。

  • const viewableThreshold = 70
  • const url = 'https://xxxxx.xxxx'
  • fd.append('view_imp', 'xxxxxx')

先ほどの遅延ロードのコードと大きくは変わりません。
isViewable()でroot要素との交差割合が70%以上になったら、計測用のリクエストを送るようになっています。

const io = new IntersectionObserver((entries, obj) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) { // 監視対象の要素が交差しているかどうか
      loadImage(entry.target); // 画像の読み込み
    }
    if (isViewable(entry.intersectionRatio)) { // 交差割合が70%以上かどうか
      console.log('viewable')
      viewableSend(); // 計測用のリクエスト
      obj.unobserve(entry.target); // 監視を解除
    }
  });
}, opts);

対応ブラウザ

対応しているブラウザに関しては下記リンクに書かれています。
最新バージョンのブラウザであればほとんど対応できています。
Intersection ObserverAPI

最後に

Google Chrome 76 からブラウザの標準機能としてlazy-loadが使えるようになっているので、 シンプルな画像の遅延ロードならばこちらを使った方が良いと思います!

Intersection Observerはいかがだったでしょうか?
個人的には簡単に利用できて使い勝手が良かったです!

最近はコードを書く機会が減ったので良いリハビリでした。
また機会があれば、次はプロダクトマネージャーとしての内容を書けたらなと思います。


 

次は永井さんの記事です。

http://blog.engineer.adways.net/entry/advent_calendar_2019/08