旧ブログ(ISSEN)から移行しました

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

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

杉田侑祐
杉田侑祐7分で読めます

はじめに

この記事の概要

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

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

Window: resize イベント - Web API | MDN

resize イベントは、この文書のビュー (ウィンドウ) の大きさが変更されたときに発行されます。

developer.mozilla.org

Document: scroll イベント - Web API | MDN

scroll イベントは、文書のビューまたは要素がスクロールされたときに発生します。 スクロールが完了したことを検出するには、 Document: scrollend イベントを参照してください。 要素のスクロールには、 Element: scroll イベントを参照してください。

developer.mozilla.org

対象読者

  • JavaScriptを使用している方

resizeイベントの改善策

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

resizeイベントの問題点

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

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

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

resizeイベントの改善

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

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

JavaScript
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イベントの問題点

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

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

皆さんの予想どおりかもしれませんが、スクロールされるたびに関数onScrollが呼び出されています。
resizeイベントと同様で頻繁にコールバック関数が呼び出されています。

scrollイベントの改善

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

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

JavaScript
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を使用しましょう!

IntersectionObserver - Web API | MDN

IntersectionObserver は交差オブザーバー API のインターフェイスで、対象となる要素と祖先要素または文書の最上位のビューポートとがの交差状態(重なり合っている状態)の変化を非同期に監視する方法を提供します。その祖先要素またはビューポートはルートと呼ばれます。

developer.mozilla.org

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

JavaScript
// 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メソッドで指定します!

JavaScript
const target = document.getElementById("square")
observer.observe(target)

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

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

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

JavaScript
// 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スクロールしたら実行という処理が可能になります!
このようにIntersectionObserverを用いることで無駄に関数を呼び出すことなく同様の実装が可能になります!

さいごに

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

この記事を書いた人

杉田侑祐
杉田侑祐

TOKOSのフロントエンドエンジニア兼UI/UXデザイナー。このブログではフロントエンドメインで投稿しています。HIPHOPとゲームが好きです✌️