addEventListener(‘scroll’)からIntersection Observer APIに乗り換える


2019年11月18日

皆さんこんにちは、末ちゃんです。
今日もまたIEに悩まされる(polyfillが必要)な話題ですが、ウェブサイトのパフォーマンス最適化に欠かせないお話しです。

スクロールイベント

ウェブサイトのコーディングを行うとき、addEventListener('scroll')を使用するシチュエーションは非常に多いのではないでしょうか?
例えば、要素がふわっと表示されてくるようなエフェクトや、要素がビューポートに入ったときにアクションが起きるなど、あらゆるところでスクロールのイベントを使用します。

そんなとき、使用するのがaddEventListener('scroll')だと思います。

しかしaddEventListener('scroll')を使用するととある問題が発生します。
それはスクロールが発生する度にイベントが発火するということです。

これはスクロール量が多いウェブサイトほど、パフォーマンスに影響を与えます。

Intersection Observer API

上記の問題を解決してくれる画期的なAPIが Intersection ObserverAPI です。
MDN Intersection Observer

日本語訳通り、交差する要素を監視してくれるAPIです。
このAPIを使用すれば、要素が指定したポイントに交差したときにのみイベントが発火してくれるため、圧倒的にパフォーマンス改善となります。

スクロールする度にイベント発火していたaddEventListener('scroll')とは雲泥の差ですね。

実例

早速実例コードを元に試していきましょう。
今回はよくあるスクロールすると、下から要素がふわっと出てくるエフェクトで試してみたいと思います。

HTML

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>TEST scroll event</title>
    <link rel="stylesheet" href="./style.css">
  </head>

  <body>
    <div class="app">
      <div class="item is-hidden"></div>
      <div class="item is-hidden"></div>
      <div class="item is-hidden"></div>
      <div class="item is-hidden"></div>
      <div class="item is-hidden"></div>
      <div class="item is-hidden"></div>
      <div class="item is-hidden"></div>
      <div class="item is-hidden"></div>
      <div class="item is-hidden"></div>
      <div class="item is-hidden"></div>
      <div class="item is-hidden"></div>
      <div class="item is-hidden"></div>
    </div>

    <script src="./index.js"></script>
  </body>
</html>

CSS

* {
  box-sizing: border-box;
}

*::before,
*::after {
  box-sizing: inherit;
}

.app {
  display: flex;
  flex-flow: row wrap;
  width: 700px;
  margin: 100vw auto;
}

.item {
  width: calc(100% / 3 - 8px * 2);
  height: 300px;
  margin: 8px;
  border-radius: 10px;
  background: rgba(0, 0, 0, .3);
  box-shadow: 0 0 10px rgba(0, 0, 0, .3);
  transition: .6s cubic-bezier(0.23, 1, 0.32, 1);
}

.item.is-hidden {
  opacity: .2;
  box-shadow: 0 0 5px rgba(0, 0, 0, .1);
  transform: translateY(20px);
}

JavaScript

addEventListener(‘scroll’) でのコード例
window.addEventListener('load', () => {
  const app = document.querySelector('.app');
  const items = app.querySelectorAll('.item');

  const windowYOffset = window.pageYOffset;
  const windowHeight = window.innerHeight;

  const itemsPosition = Array.from(items).map((x) => x.getBoundingClientRect());
  const itemsPositionTop = itemsPosition.map((x) => x.top + windowYOffset);

  const threshold = 200;

  window.addEventListener('scroll', () => {
    const currentYOffset = window.pageYOffset;

    items.forEach((item, index) => {
      if (currentYOffset > itemsPositionTop[index] - windowHeight + threshold) {
        item.classList.remove('is-hidden');
      }
    });

    console.log('fired');
  });
});

まずはaddEventListener('scroll')の例からです。

要素のビューポートの最上部からの位置を取得したり、ウインドウの高さを取得したり、やることが多くて非常にテンションが下がりますね。
また、スクロールする度に現在のスクロール位置も取得しなければいけないため、結構負荷が大きそうです。

実際に実行してみたものが下の動画です。
最終的に188回イベントが発火しているのがわかります。

Intersection Observer API でのコード例
window.addEventListener('load', () => {
  const app = document.querySelector('.app');
  const items = app.querySelectorAll('.item');

  const callback = (entries) => {
    entries.forEach((entry) => {
      if (!entry.isIntersecting) return null;

      const el = entry.target;
      el.classList.remove('is-hidden');

      console.log('fired');
    });
  };

  const options = {
    root: null,
    rootMargin: '0px',
    threshold: .5,
  };

  const observer = new IntersectionObserver(callback, options);

  items.forEach((item) => {
    observer.observe(item);
  });
});

Intersection Observer APIでの実例です。

callback関数は、発火する処理を書いています。
optionsにIntersection Observer APIのための設定を書き込んでいます。

すごいシンプルだと思いません・・・?
ビューポートの高さを取得したり、要素の位置を取得したりなど何もありません。

実際に動かしてみた動画が以下です。
発火したイベントは24回です。全然違います。

まとめ

実際の使い方については他のサイトや、MDNで詳しく書いてあるのでそちらでの参照をお願いします。(手抜きでは・・・)
https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API

ただIEが相変わらず動作しないので、polyfillが必要なのでご了承ください・・・。

それではまたお会いしましょう。

Share Button