Intersection Observerを触ってみた

#Intersection Observerとは
指定したDOM要素の交差点（Intersection）を監視するAPIです。

#Intersection Observerのメリット
従来、DOM要素の位置計算や監視は、scrollイベントを使用し、scrollTopやrectTopなどで行っていました。
しかし、以下のような問題点があります。

- Forced Synchronous Layoutが発生する（レイアウトの再計算、詳しくはこちら　[What forces layout / reflow](https://gist.github.com/paulirish/5d52fb081b3570c81e3a)）
- Scroll Jankの可能性
- そもそもの実装が面倒

Intersection Observerを使用することで、位置の監視をブラウザが行ってくれるため、パフォーマンスの改善につながります。

#サンプル
従来の手法とIntersection Observer APIを使った手法で、画像遅延読み込みのサンプルを実装しました。

##従来の手法
```html:none_observer.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title></title>
  <style>
    .image {
      width: 400px;
      padding: 10px;
      background: #efefef;
      margin: 0 auto 20px auto;
    }
    .image img {
      display: block;
    }
  </style>
</head>
<body>
  <div class="image" data-image="image01.jpg">
    <img src="" alt="" width="400" height="284">
  </div>
  <div class="image" data-image="image02.jpg">
    <img src="" alt="" width="400" height="284">
  </div>
  <div class="image" data-image="image03.jpg">
    <img src="" alt="" width="400" height="284">
  </div>
  <div class="image" data-image="image04.jpg">
    <img src="" alt="" width="400" height="284">
  </div>
  <div class="image" data-image="image05.jpg">
    <img src="" alt="" width="400" height="284">
  </div>
  <div class="image" data-image="image06.jpg">
    <img src="" alt="" width="400" height="284">
  </div>
  <div class="image" data-image="image07.jpg">
    <img src="" alt="" width="400" height="284">
  </div>
  <div class="image" data-image="image08.jpg">
    <img src="" alt="" width="400" height="284">
  </div>
  <div class="image" data-image="image09.jpg">
    <img src="" alt="" width="400" height="284">
  </div>
  <div class="image" data-image="image10.jpg">
    <img src="" alt="" width="400" height="284">
  </div>
  <div class="image" data-image="image11.jpg">
    <img src="" alt="" width="400" height="284">
  </div>
  <div class="image" data-image="image12.jpg">
    <img src="" alt="" width="400" height="284">
  </div>
  <script>
    (function() {

      const ROOT_MARGIN = 200;

      const images = document.querySelectorAll(".image");

      // イベントの間引き
      const throttle = (func, wait)=> {
        let time = Date.now();
        return ()=> {
          if ((time + wait - Date.now()) < 0) {
            func();
            time = Date.now();
          }
        }
      }

      class LazyLoad {
        constructor(image) {
          this.isLoaded = false;

          this.windowHeight = window.innerHeight;

          this.image = image;
          this.rect = this.image.getBoundingClientRect();

          this.onScroll = this.onScroll.bind(this);
        }
        addImage() {
          const imagePath = "img/" + this.image.getAttribute("data-image");
          this.image.children[0].setAttribute("src", imagePath); 
        }
        checkImageLoad() {
          let point = this.rect.top - ROOT_MARGIN;
          if(this.windowHeight > point) {
            this.addImage();
            this.isLoaded = true;
          }
        }
        onScroll() {
          if(this.isLoaded) return false;
          this.rect = this.image.getBoundingClientRect(); //スクロールの度に画面全体の再レイアウトが発生
          this.checkImageLoad();
        }
        addEventListener() {
          window.addEventListener("scroll", throttle(this.onScroll, 100), {passive: true});
        }
        init() {
          this.checkImageLoad();
          this.addEventListener();
        }
      }

      Array.from(images).map(image => {
        const lazyLoad = new LazyLoad(image);
        lazyLoad.init();
      });

    }());

  </script>
</body>
</html>
```

scrollイベントで監視を行い、対象のDOMがviewportの中に入ったらcallbackを実行しています。scrollの度に位置を取得しているため、画面全体のレイアウト（リフロー）が発生しパフォーマンスに影響を与えます。

##Intersection Observerでの手法

html/cssは上記のソースコードと同じため、Javascript部分のみ記載します。

```javascript:observer.js
(function() {

  const ROOT_MARGIN = '200px 0px';

  const images = document.querySelectorAll(".image");

  const intersectionChanged = (entries) => {
    for (let entry of entries) {
      let scopeElm = entry.target;
      let imagePath = "img/" + scopeElm.getAttribute("data-image");
      if(entry.isIntersecting) {
        scopeElm.children[0].setAttribute("src", imagePath); 
      }
    }
  }

  class LazyLoad {
    constructor(image) {
      this.image = image;
    }
    init() {
      let observer = new IntersectionObserver(intersectionChanged, {
        root: null,
        rootMargin : ROOT_MARGIN
      });
      observer.observe(this.image);
      }
    }
      
  Array.from(images).map(image => {
    let lazyLoad = new LazyLoad(image);
    lazyLoad.init();
  });

}());
```
従来の手法でいうscroll時の監視を、全てIntersectionObserverクラスが行ってくれます。

###解説
Intersection Observerにはいくつかのコールバック引数と、3つのオプションがあります。

####コールバック引数

```javascript:observer_callback.js
const intersectionChanged = (entries) => {
  for (let entry of entries) {

    // 変更が起こったタイムスタンプ
    console.log(entry.time);               

    // ルートのgetBoundingClientRect()
    console.log(entry.rootBounds);         

    // ターゲットのgetBoundingClientRect()
    console.log(entry.boundingClientRect); 

    // 交差領域のgetBoundingClientRect()
    console.log(entry.intersectionRect);

    // 交差している領域の割合
    console.log(entry.intersectionRatio);

    //交差しているかどうか
    console.log(entry.isIntersecting);

    // ターゲット
    console.log(entry.target);

  }
}
```

####オプション


```javascript:observer_option.js
let observer = new IntersectionObserver(intersectionChanged, {

  // 交差対象のDOMの指定。初期値はdocument（viewport）。
  root: document.getElementbyid('target'),

  // entry.intersectionRatioを呼び出すタイミングを指定できる（下記は10%ごとに呼び出す）
  threshold: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],

  // viewportと交差する何px前に呼び出したい場合や交差後何px後に呼び出したい場合に指定（下記は交差前200pxの部分でコールバック）
  rootMargin: "200px"

});
observer.observe(target);

```
---

対応ブラウザは相当限られるので使用の際はご注意ください。
（17/5/22現在、Chromeのみ、Node.jsのローカルサーバーで確認）

以下の記事を参考にさせていただきました。
[Intersection Observer を用いた要素出現検出の最適化](https://blog.jxck.io/entries/2016-06-25/intersection-observer.html)