【jQuery】スクロールで要素が表示されたら実行する完全ガイド|アニメーション・複数要素・パフォーマンス最適化まで

スクロールして要素が画面内に入ったタイミングでアニメーションを起動したり、遅延読み込みを行うパターンはLPやポートフォリオサイトで広く使われています。この記事ではjQueryでの実装からパフォーマンス最適化・複数要素の一括管理・IntersectionObserverとの使い分けまで体系的に解説します。

この記事でわかること

  • スクロール位置と要素位置を比較して表示を検知する基本パターン
  • 一度だけ実行する方法(クラス付与で重複防止)
  • フェードイン・スライドインアニメーションとの組み合わせ
  • 複数要素をeach()でまとめて監視する
  • debounceでスクロールイベントを最適化する
  • IntersectionObserver APIとの比較・使い分け
スポンサーリンク

スクロール表示検知の基本パターン

スクロールで要素が表示されたかどうかは、「ウィンドウの上端からのスクロール量 + ウィンドウの高さ」が「要素の上端位置」を超えたときが画面内に入った瞬間です。

計算の仕組み

jQueryの取得方法 意味
ウィンドウ上端のスクロール量 $(window).scrollTop() ページ上部からどれだけスクロールしたか(px)
ウィンドウの高さ $(window).height() ブラウザの表示領域の高さ(px)
要素の上端位置 $(el).offset().top ページ上部から要素の上端までの距離(px)

「スクロール量 + ウィンドウ高さ ≧ 要素の上端位置」が成立すれば要素が画面内に入っています。

基本の実装

<div id="target-section">
  <p>スクロールして表示されると実行される要素</p>
</div>
$(function () {
  function checkVisible() {
    var scrollBottom = $(window).scrollTop() + $(window).height();
    var elemTop      = $('#target-section').offset().top;

    if (scrollBottom >= elemTop) {
      // 要素が画面内に入ったときの処理
      console.log('要素が表示されました');
    }
  }

  $(window).on('scroll', checkVisible);
  checkVisible();  // 初期表示時にも実行(ページ読み込み時に既に見えている場合)
});
初期表示時にもcheckVisible()を呼ぶ
スクロールせずに最初から要素が見えているケース(ファーストビュー内の要素)にも対応するため、ページ読み込み直後に一度 checkVisible() を呼んでおくことが重要です。

一度だけ実行する(重複防止)

スクロールイベントは連続して発火します。アニメーションや重い処理を表示時に一度だけ実行したい場合は、実行済みフラグとしてクラスを付与する方法が確実です。

$(function () {
  function checkVisible() {
    var $el = $('#target-section');

    // すでに実行済みならスキップ
    if ($el.hasClass('is-visible')) return;

    var scrollBottom = $(window).scrollTop() + $(window).height();
    var elemTop      = $el.offset().top;

    if (scrollBottom >= elemTop) {
      $el.addClass('is-visible');  // フラグを立てる
      // ここに処理を書く
      console.log('初回のみ実行');
    }
  }

  $(window).on('scroll', checkVisible);
  checkVisible();
});
全要素が実行済みになったらスクロールイベントを解除する
処理が全て完了したあともスクロールイベントが発火し続けるのは無駄です。$(window).off("scroll", checkVisible) でイベントを解除することでパフォーマンスを改善できます。

フェードイン・スライドインアニメーションとの組み合わせ

スクロール検知と組み合わせて最もよく使われるのはフェードインアニメーションです。CSSと組み合わせてスライドインも実現できます。

フェードインアニメーション

.fade-target {
  opacity: 0;  /* 初期状態は非表示 */
}
$(function () {
  function checkFade() {
    var scrollBottom = $(window).scrollTop() + $(window).height();

    $('.fade-target:not(.is-visible)').each(function () {
      if (scrollBottom >= $(this).offset().top) {
        $(this).addClass('is-visible').fadeTo(600, 1);
      }
    });
  }

  $(window).on('scroll', checkFade);
  checkFade();
});

下からスライドインアニメーション(CSSトランジション版)

.slide-target {
  opacity: 0;
  transform: translateY(40px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}

.slide-target.is-visible {
  opacity: 1;
  transform: translateY(0);
}
$(function () {
  function checkSlide() {
    var scrollBottom = $(window).scrollTop() + $(window).height();

    $('.slide-target:not(.is-visible)').each(function () {
      if (scrollBottom >= $(this).offset().top + 50) {  // 50px手前で発火
        $(this).addClass('is-visible');
      }
    });
  }

  $(window).on('scroll', checkSlide);
  checkSlide();
});
offset().top + 50px で早めに発火させる
要素の上端ちょうどでアニメーションを開始すると、要素が画面下ギリギリで見えた瞬間に動くため視覚的に遅れて見えることがあります。elemTop + 50 のようにオフセットを加えると少し手前でアニメーションが始まり自然な印象になります。スクロールに合わせたフェードインの応用例はスクロールに合わせたフェードインアニメーションも参照してください。

複数要素をeach()でまとめて監視する

セクションごとにIDを使って個別に監視するのではなく、共通クラスを使って複数要素をまとめて管理するのが実務的なパターンです。

<div class="scroll-reveal">セクション1</div>
<div class="scroll-reveal">セクション2</div>
<div class="scroll-reveal">セクション3</div>
$(function () {
  function checkReveal() {
    var scrollBottom = $(window).scrollTop() + $(window).height();
    var allDone = true;

    // is-visibleが付いていない要素だけ処理
    $('.scroll-reveal:not(.is-visible)').each(function () {
      allDone = false;  // まだ未処理の要素がある

      if (scrollBottom >= $(this).offset().top + 30) {
        $(this).addClass('is-visible');
      }
    });

    // 全要素が処理済みになったらイベントを解除
    if (allDone) {
      $(window).off('scroll', checkReveal);
    }
  }

  $(window).on('scroll', checkReveal);
  checkReveal();
});

debounceでパフォーマンスを最適化する

スクロールイベントは1秒間に数十〜数百回発火します。複雑な処理をそのまま実行するとページがカクつく原因になります。debounce(一定時間後に一度だけ実行)またはthrottle(一定間隔で実行)で発火頻度を制限します。

シンプルなthrottleの実装

// throttle: 指定ミリ秒ごとに最大1回だけ実行
function throttle(fn, wait) {
  var lastTime = 0;
  return function () {
    var now = Date.now();
    if (now - lastTime >= wait) {
      lastTime = now;
      fn.apply(this, arguments);
    }
  };
}

$(function () {
  function checkReveal() {
    var scrollBottom = $(window).scrollTop() + $(window).height();
    $('.scroll-reveal:not(.is-visible)').each(function () {
      if (scrollBottom >= $(this).offset().top + 30) {
        $(this).addClass('is-visible');
      }
    });
  }

  // 100msに1回だけ実行(発火頻度を制限)
  $(window).on('scroll', throttle(checkReveal, 100));
  checkReveal();
});
debounceとthrottleの使い分け

  • throttle: スクロール中に定期的に実行したい場合(スクロール連動アニメーションなど)。100〜200ms が目安
  • debounce: スクロールが止まってから実行したい場合(検索・API呼び出しなど)。200〜300ms が目安
  • スクロール表示検知のようなリアルタイム性が必要な処理には throttle が適しています

IntersectionObserver APIとの比較・使い分け

現代のブラウザでは IntersectionObserver APIがサポートされています。jQueryのscrollイベント方式との特徴を比較します。

方法 パフォーマンス コード量 IE対応 向いている用途
jQueryのscroll() 普通(throttle必須) 少なめ jQuery使用プロジェクト・IE11対応必要な場合
IntersectionObserver 高い(ブラウザネイティブ) 中程度 △(ポリフィル必要) 新規プロジェクト・大量要素の監視

IntersectionObserverの基本実装(参考)

// IntersectionObserver版(jQueryなしでも動作)
var observer = new IntersectionObserver(function (entries) {
  entries.forEach(function (entry) {
    if (entry.isIntersecting) {
      entry.target.classList.add('is-visible');
      observer.unobserve(entry.target);  // 一度検知したら監視解除
    }
  });
}, {
  threshold: 0.1  // 10%見えたら発火
});

// 監視対象を登録
document.querySelectorAll('.scroll-reveal').forEach(function (el) {
  observer.observe(el);
});
IntersectionObserverのIE11サポート
IE11ではIntersectionObserverがサポートされていません。IE11をサポートする必要がある場合はjQueryのscrollイベント方式を使うか、ポリフィル(intersection-observer npmパッケージ)を導入してください。現在(2024年以降)は多くのプロジェクトでIE11サポートを終了しているため、IntersectionObserverを積極的に採用できます。

まとめ

jQueryでスクロール表示検知を実装するには、$(window).scrollTop() + $(window).height()$(el).offset().top を比較するのが基本です。クラス付与で重複実行を防ぎ、throttleでパフォーマンスを最適化し、全要素処理後はイベントを解除するのが実務的なパターンです。新規プロジェクトではIntersectionObserver APIも検討してください。

関連記事: スクロールに合わせたフェードインアニメーション / jQueryアニメーション完全ガイド / 画像を1枚ずつフェードインする完全ガイド

よくある質問(FAQ)

Qページ読み込み直後に要素が見えているのにアニメーションが動きません。
Ascrollイベントはスクロールが発生しないと発火しないため、ページ読み込み時に既に見えている要素は検知されません。関数を定義したあとに checkReveal() を一度直接呼び出すことで対応できます。
Qスマートフォンでアニメーションがカクつきます。
Ascrollイベントの発火頻度が高いことが原因です。throttleで発火頻度を100〜200msに制限してください。またCSSアニメーションには will-change: opacity, transform をCSSに追加するとGPUが活用されスムーズになります。根本的な解決策としてIntersectionObserverへの移行もご検討ください。
Q要素が固定ヘッダー(sticky header)に隠れた状態でアニメーションが発火します。
A固定ヘッダーの高さ分だけオフセットを加算してください。たとえばヘッダーが60pxの場合は $(window).scrollTop() + $(window).height() - 60 で比較するか、elemTop + 60 >= scrollBottom の条件に調整します。
Qモーダル内やスクロールコンテナ内の要素を監視したいです。
A$(window).on("scroll", ...) はウィンドウのスクロールのみ検知します。スクロールコンテナ(overflow: auto の要素)内の要素を監視するには、$(".scroll-container").on("scroll", ...) のようにコンテナ要素のscrollイベントを使うか、IntersectionObserverに root: document.querySelector(".scroll-container") を指定してください。
Qスクロールを上に戻したときに要素を非表示に戻したいです。
Aクラスを付けたら外さない方式では一度表示したら戻りません。上スクロールでも検知するには、scrollBottom < elemTop の条件でremoveClass("is-visible") を実行してください。ただし、一般的なスクロールアニメーションは「一度見えたら保持」が多く、戻す実装はUXが複雑になるため慎重に検討してください。