[JavaScript] スクロールイベントにPassive Event Listener指定してパフォーマンスを向上させる方法

5 min read

DOM の新仕様として、スクロールのパフォーマンスを改善するためにaddEventListenerPassive Event Listeners というオプションが追加された。

Scroll Jank

ページのスクロール時に発生する(スクロールが詰まったような)遅延を「Scroll Jank」と呼ぶ。 こういった Scroll Jank は、スクロールやタッチイベントリスナーに原因がある。

イベント内でpreventDefault()を実行した場合、デフォルトのイベントはキャンセルされる。
現在ブラウザは、イベント内でpreventDefault()が実行されるか否かは、そのイベントが実行が終了するまで判定ができないため、イベント内の処理が終了するの待つことになる。

スクロールイベントもpreventDefault()が実行された場合は、スクロールはキャンセルされるが、同様にイベント内でpreventDefault()が実行されるか否かを判定できるまでスクロールが止まることになる(遅延が発生する)。

これが Scroll Jank が発生する主な原因である。

EventListenerOptions passiveとは

処理実行前に「preventDefault()を実行していない」ことが判定できれば、Scroll Jank の問題は解決できる。こういった中でaddEventListenerPassive Event Listeners というオプションが追加された。

追加されたオプションは、addEventListenerの第三引数にoptions({passive: true})を指定する事で「処理がpreventDefault()を実行していない」という事が明示できるようになった。
これより、スクロールイベントのリスナーにこのオプションを指定することで処理終了後ではなく、スクロールをすることができるようになった。

document.addEventListener("touchmove", func, { passive: true });

options > passive: listenerpreventDefault()を呼び出さないことを表す Boolean 値です。
trueが指定された状態でlistenerpreventDefault()を呼び出すと、ユーザーエージェントはその呼び出しを無視し、コンソールに警告を出力します。

EventTarget.addEventListener() - Web API インターフェイス | MDN

wheelmousewheeltouchstarttouchmovepassive 指定をすると良い。

非対応ブラウザとの互換の問題

元々addEventListenerの第三引数には、useCaptureが定義されていた。useCaptureの説明は割愛するが、今後useCaptureを指定する場合は、{capture: true}といった形で指定する。

モダンブラウザの殆どが Passive event listener に対応しているが、Can I use… Support tables for HTML5, CSS3, etcを見たら分かるように Internet Explorer 11 だけが未対応となっている。
もし、こういった非対応ブラウザの第三引数にoptionsの Object を渡してしまうと、useCapturetrue評価になってしまう。

非対応ブラウザでuseCaptureが意図しない指定になるのはよろしくはないので、回避したい場合は Passive event listener に対応しているのか判定が必要になる。

非対応ブラウザ向けに判定処理を実装する事が出来る。EventTarget.addEventListener() - Web API インターフェイス | MDNには、こういった判定処理が紹介されている。

/* "passive" が使えるかどうかを検出 */
var passiveSupported = false;

try {
  window.addEventListener(
    "test",
    null,
    Object.defineProperty({}, "passive", {
      get: function () {
        passiveSupported = true;
      },
    })
  );
} catch (err) {}

/* リスナーを登録 */
var elem = document.getElementById("elem");

elem.addEventListener(
  "touchmove",
  function listener() {
    /* do something */
  },
  passiveSupported ? { passive: true } : false
);

Passive event listener を jQuery で対応するには…

現状、ない。

おわり

要素検出や要素固定など本来スクロールに直接関係のない処理については、Intersection Observerposition: stickyの登場でスクロールイベント内で処理をせず、負荷の少ない実装をする事が可能になった。

だが、それでもスクロールイベントでの処理が必要な場合は少なくない。そういう場合はこういった手法を使うのが定石となってくるだろう。