【JavaScript】resize・scrollイベントを使う際の注意点と改善策

はじめに

この記事の概要

こんにちは、株式会社TOKOSのスギタです!
本日はJavaScriptのイベントリスナーの「resize」「scroll」を別のAPIやイベントハンドラーを使ってパフォーマンス改善をしていこうと思います!

resize・scrollイベントがわからない方は下記を参照して下さい。

対象読者

  • JavaScriptを使用している方

resizeイベントの改善策

まずはresizeイベントの問題点を考えてみます。

resizeイベントの問題点

まずは下記コードを実行してどんなときに実行されるか見てみましょう

const onResize = () => console.log("画面サイズが変わりました!");
window.addEventListener("resize", onResize);

上記関数を実行した際のコンソールで見ると画面幅が変わるたびにコールバック関数であるonResize関数が呼ばれているかと思います。
windowサイズが変わるたび1pxでも変化したタイミングで呼ばれるためパフォーマンスの観点では良いとは言えません!
今回はonResize関数の中身はコンソールでテキストを表示するだけですが、重い処理などの場合パフォーマンスに影響してしまいます!

resizeイベントの改善

resizeイベントの問題点はwindowサイズが変わるたびに呼ばれて必要以上に呼ばれてしまうことでした!
では改善策として、時間で区切りを設けコールバック関数の呼ばれる回数を制御します。

今回は関数throttleを作成してonResizeをラップします!

const onResize = () => console.log("画面サイズが変わりました!");

const throttle = (func, interval) => {
  let time = Date.now() - interval;
  return () => {
    if (time + interval < Date.now()) {
      time = Date.now();
      func();
    }
  };
};

window.addEventListener("resize", throttle(onResize, 50));

この関数では2つの引数を受取ます。

  • 第一引数 : 実行したい関数(今回の場合はonResize関数)
  • 第二引数 : 遅延をかけたい秒数

まず変数timeは現在の秒数から引数で受け取った50ミリ秒を引きます。
そして条件文分岐のtime + interval < Date.now() で最後に関数が呼び出されてから指定したinterval(今回の例では50ミリ秒)が経過しているかを確認しています。
この条件がtrueであれば、現在の時刻をtimeに更新し、第一引数で渡した関数(func)を実行します。

こうすることにより50ミリ秒に1回しか実行されないように制御できます。
頻繁な関数の呼び出しを防ぐことが可能になります!

scrollイベントの改善策

まずはscrollイベントの問題点を考えてみます。

scrollイベントの問題点

まずは下記コードを実行してどんなときに実行されるか見てみましょう

const onScroll = () => console.log("スクロールしました!");
document.addEventListener("scroll", onScroll);

多分皆さん予測できたでしょう、スクロールされるたびに関数onScrollが呼び出されています。
resizeイベントと同様で頻繁にコールバック関数が呼び出されています。

scrollイベントの改善

先ほどresizeイベントの改善の際に紹介したと同様、頻繁に呼び出されるのを防ぐのであればthrottle関数で良いでしょう。
では、scrollイベントを使用する場合の実例を考えてみましょう!

実際scrollイベントを使用する際は「〇〇pxスクロールしたら何かする」のように使用すると思います。
ではscrollイベントを使用して400pxスクロールしたら四角を表示してみましょう!

const onScroll = () => {
  // スクロール位置の取得
  const scrollTop = window.scrollY || document.documentElement.scrollTop;
  console.log("スクロールしてるよ");

  if (scrollTop >= 400) {
    document.getElementById("square").style.display = "block";
  } else {
    document.getElementById("square").style.display = "none";
  }
};

document.addEventListener("scroll", onScroll);

上記コードのようにスクロールするたびにスクロール位置の取得と計算をさせ、条件分岐で実行したい内容を記述すると思います。
コンソールでも見れる通り、不必要にスクロール位置の取得と計算をさせるのはあまり望ましくありません。

「〇〇pxスクロールしたら何かする」の場合ではIntersectionObserver を使用しましょう!

IntersesectionObserverとは

定の要素がビューポート内に入ったり出たりすることを効率的に監視するためのAPIです。これにより、スクロールイベントを使うよりもパフォーマンスが向上し、スクロール連動のアニメーションやLazy Load(遅延読み込み)などを実装するのに便利です。

下記は基本的な使い方です。

// Intersection Observerのインスタンスを作成
  const observer = new IntersectionObserver((entries, observer) => {
    // entriesは監視対象の各要素の状態を表すIntersectionObserverEntryオブジェクトの配列
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        // 要素がビューポートに入ったときの処理
        console.log('要素が表示されました:', entry.target);
        entry.target.style.backgroundColor = 'red';
      } else {
        // 要素がビューポートから出たときの処理
        entry.target.style.backgroundColor = 'blue';
      }
    });
  }, {
    // オプション設定(省略可能)
    root: null, // ビューポートを基準に監視
    rootMargin: '0px',
    threshold: 0.5 // 50%表示されたらコールバックを呼び出す
  });

  // 監視したい要素を取得
  const target = document.getElementById('square');

  // その要素を監視
  observer.observe(target);

IntersectionObserverのインスタンスは、監視したい要素がビューポートに入ったり出たりするたびに呼び出されるコールバック関数を受け取ります。

  • entries : 監視対象の要素の状態を表すIntersectionObserverEntryオブジェクトの配列です。
  • observer : 現在のIntersectionObserverインスタンスです。

第二引数にはオプション設定を渡すことができます!
これには3つの主要プロパティがあります。

root :

  • 監視の基準となる要素を指定します。
  • nullの場合はビューポート(表示されている部分)が基準になります。

rootMargin :

  • rootの周りのマージンを指定します。CSSのマージンと同じ形式で指定します
  • 例: 0px 0px -50px 0px

threshold :

  • コールバックを呼び出すための閾値を指定します。要素がどれくらい表示されたかの割合(0.0から1.0)を指定します。
  • 例: 0.5 は、要素が50%表示されたときにコールバックを呼び出します

また監視したい要素をobserveメソッドで指定します!

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

これで、target要素がビューポートに入ったり出たりするたびに、コールバック関数が実行されるようになります!

ではIntersectionObserverの基本的な使い方は以上とします。基本的にはスクロール時指定した要素が画面内に入ったら何か実行のような使い方になります。

ですが、今回の使用方法は画面上部から400pxスクロールしたら実行なので紹介した例から工夫しないといけません。optionの指定が肝心になってきます!

// Intersection Observerのオプション設定
const option = {
  root: null, // ビューポートを基準に監視
  rootMargin: "-400px 0px 0px 0px", // ビューポートの上から400pxの位置で発火
  threshold: 1.0,
};

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (!entry.isIntersecting) {
      //400pxスクロールされたら実行
      document.getElementById("square").style.display = "block";
    } else {
    //400pxスクロールされていない場合に実行
      document.getElementById("square").style.display = "none";
    }
  });
}, option);

// 監視対象の指定
observer.observe(document.body);

optionrootMarginをマイナスにすることにより画面上部から400pxスクロールしたら実行という処理が可能になります!
このようにIntersctionObserverを用いることで無駄に関数を呼びだすとなく同様の実装が可能になります!

さいごに

今回はイベントリスナーのresizeイベントとscrollイベントを使用する際の改善案を紹介しました。
細かい積み重ねがよりよいUXにつながるので、パフォーマンスを意識した実装ができると良いですね!