スクロールイベントの処理ならIntersectionObserver(交差オブザーバー)を使おう(コピペコード付)

数年前のブログで、アニメーションの制御にはjQueryのスクロールイベントを使用していましたが、この方法はスクロール中に連続してイベントが処理されるため、パフォーマンスの観点からはイベントの間引き処理などの工夫が必要です。

Chromeのデベロッパーツールで以下のコードを試しに実行し、ピコサプのトップページを上から下までスクロールしてみると、イベントの処理がなんと184回も発生しました。

document.addEventListener('scroll',() => {console.log('イベント発生')});
イベントが184回も発生した・・・

またレスポンシブ対応などで要素の高さが変わってしまった場合など、イベント位置の再計算が必要になるためさらにresizeイベントなどで再計算する必要もありました。

スマホやPCの性能が高い現在ではこの程度でもたつくことはないと思いますが、イベントで処理するものの負荷が高くなるほど無視できないものになっていきます。なのでこのような無駄はなくしていきたいですね。

今は交差オブザーバーと呼ばれる便利なものがありますのでこれを使っていきます。

IntersectionObserver(交差オブザーバー)とは

交差オブザーバーは、指定した監視する要素が設定した監視範囲(通常はビューポート)と交差した時(入った時と出た時)に、あらかじめ指定した処理を行う仕組みです。要素を指定するだけなので要素のサイズが変わってしまっても問題なく機能します。

イベント処理は交差した瞬間のみのため、スクロールする度にイベントが発生していたscrollイベントと比べるとパフォーマンスの観点から非常に優れています。現在は主要ブラウザでは対応済みですが、サポートされていないInternet Explorerが現役の時は注意が必要でした。

実際の記述

IntersectionObserverにコールバック関数とオプション指定してインスタンス化します。

コールバック関数は実際に交差した時の処理を記述し、交差範囲の設定はオプションで行います。

const options = {
  root: null,
  rootMargin: '0px',
  threshold: 1.0
}

const observer = new IntersectionObserver(callback, options);

この段階はまだ監視する要素が指定されていません。
監視する要素はインスタンス化したobserverオブジェクトのobserveメソッドで指定します。

const target = document.getElementById('target');
observer.observe(target);

コールバック関数

ターゲット要素がビューポート内に重なった時と外れた時に処理する内容をコールバック関数として記述します。交差状態をを示すオブジェクトの配列が引数として格納されています。

このオブジェクトの中に交差状態を示すisIntersectingがあります。
このプロパティは交差状態に入った時にtrueを、抜けた時にfalseを返すためこれを判定して必要な処理を記述します。

const callback = entries => {
  entries.forEach((entry) => {
     if (entry.isIntersecting) {
       // 交差した瞬間の処理
     } else {
       // 交差を外れた瞬間の処理
     }
  });
};

上記のコードで引数entriesをforEachでループさせているのですが、ターゲット要素が1つであってもentriesは交差状態を示すオブジェクトは必ず配列として返されるためです。1つの場合はentries[0]のようにインデックス番号で指定しても良いと思います。

ビューポートオプション

このオプションでは監視対象の範囲を3つのプロパティで変更することができます。

const options = {
  root: null, // nullの場合はデフォルトでブラウザーのビューポートが指定されます。
  rootMargin: '0px', // 監視範囲のマージン設定(拡大・縮小のための)
  threshold: 0 // 閾値
}

root

監視される範囲です。基本的にnull(デフォルト値)を指定してとビューポートが指定されるのでそのままにして置くことが多いですが、特定の要素を範囲として指定することもできます。

rootMargin

rootで指定した監視範囲を"10px 20px 30px 40px"のようにCSSのmarginに似た値で指定します。パーセントでも指定できます。マイナスも指定することができ、イベント処理の位置を遅らせたい場合などに記述します。既定値はすべてゼロとなっています。ちなみに0を指定する場合、CSSでは単位不要で記述できますが、ここでは必ず単位をつける必要があるので注意が必要です。

threshold

ターゲット要素がどのくらい見えたら処理されるかを指定する閾値(しきい値)です。0〜1の間を数値または[0,0.25,0.5,0.75]のように配列形式で入力します。

デフォルトは0で1pxでも交差したタイミングで処理が入り、0.5だと要素の半分が交差したタイミングになります。
1を指定すると完全に要素が重なった時に処理されますが、ターゲット要素のサイズがビューポートを超えてしまうと処理が入らなくなってしまうため注意が必要です。
[0,0.25,0.5,0.75]のような配列を指定するとそれぞれのタイミングで処理が入るようになります。

モジュール化しておくと便利(コード有り)

IntersectionObserverは途中から固定ボタンを表示させたり、ヘッダーを固定させたりするのに使っています。私が通常使用する時は以下のようにのクラスでモジュール化して使用しています。

/**
 * ScrollObserverクラス
 */
class ScrollObserver {

  /**
   * @param {*} observedElement 
   * 監視する対象の要素
   * 
   * @param {*} className  (option)
   * イベントを検知した時にルート要素に追加するクラス名
   * 
   * @param {*} observerOptions (option)
   * 交差オブザーバーオプションの設定
   */
  constructor( observedElement, className = 'is-scrolled', observerOptions ) {
    
    this.observedElement = observedElement;
    this.className = className;
    this.rootClassList = document.documentElement.classList;
    this.observerOptions = {
      ...{
        root: null,
        rootMargin: '0px',
        threshold: 0
      },
      ...observerOptions
    };
    this.init();
  }
  
  init() {

    this.observer = new IntersectionObserver(entries => {

      entries.forEach(entry => {

        if (entry.isIntersecting) {

          this.setClassName();

        } else {

          this.removeClassName();

        }

      });

    }, this.observerOptions);

    this.observer.observe(this.observedElement);
  }

  /**
   * ルート要素にクラス名をセット
   */
  setClassName() {
    this.rootClassList.add(this.className);
  }

  /**
   * ルート要素からクラス名を除去
   */
  removeClassName() {

    this.rootClassList.remove(this.className);

  }
}

クラスを読み込んだ後に以下のように監視する要素を指定してインスタンス化することで、ターゲットが画面内に入った時はルート要素<html>にクラス名「is-scrolled」を付与してくれます。

オプションとして、クラス名やビューポートも役割によって指定できるようにしています。

ScrollObserver(要素[element], (任意)付与するクラス名[string], (任意)ビューポートオプション[object]);
// スクロールでルートにクラス名を付与する設定
const observedElement = document.getElementById('scroll-observer'); 
const observer = new ScrollObserver(observedElement); // インスタンス化

これを利用して以下のような、途中で表示されるロゴを実装することができました。

See the Pen Scroll Observer by hori-ak (@hori-ak) on CodePen.

実装のポイント

監視する対象ですが、実装例では#scroll-observerを指定していました。この要素は中のコンテンツとなる部分「l-main」クラス外に置いており、position:absoluteでページの下から1000pxの位置に配置しています。

このようにメインコンテンツと切り離して独立させることで、イベントの処理をコンテンツに依存せず同じ位置で処理させることができます。固定ヘッダーや固定ボタンの切替えを処理する時に便利な方法なのでおすすめです。