JavaScript | DOM 操作:イベント発展 – スクロールイベント

JavaScript JavaScript
スポンサーリンク

スクロールイベントとは何か

スクロールイベントは、ページや特定のスクロール可能な要素のスクロール位置が変わった瞬間に発火する合図です。window(ページ全体)にも、overflow: auto などでスクロール可能にした要素にも付けられます。ここが重要です:スクロールは高頻度で連続発火します。必ず軽い処理にし、必要なら間引き(throttle / debounce)や requestAnimationFrame を使って滑らかさを保ちます。


基本の使い方(window と要素)

ページ全体のスクロールを監視する例

<div style="height:2000px"></div>
<script>
  window.addEventListener("scroll", () => {
    const y = window.scrollY;       // ページ上端からのスクロール量
    const vh = window.innerHeight;  // ビューポート高さ
    console.log("scrollY:", y, "viewport:", vh);
  }, { passive: true });
</script>
HTML

ここが重要です:window のスクロールは { passive: true } を基本にします。これによりブラウザが最適化し、カクつきが減ります。スクロールを“止める”必要がある特殊な場面だけ passive を外します。

特定の要素に対してスクロールを監視する例

<div id="panel" style="height:140px; overflow:auto; border:1px solid #ccc">
  <div style="height:600px"></div>
</div>
<script>
  const panel = document.getElementById("panel");
  panel.addEventListener("scroll", () => {
    console.log("top:", panel.scrollTop, "height:", panel.clientHeight);
  }, { passive: true });
</script>
HTML

ここが重要です:要素のスクロール位置は scrollTop、表示領域は clientHeight、コンテンツ全体の高さは scrollHeight で取得します。ページ単位とはプロパティが異なる点に慣れましょう。


スクロール位置と寸法の取り方(正確な割合計算)

進捗割合の計算(ページ全体)

<script>
  function getProgress() {
    const y = window.scrollY;
    const vh = window.innerHeight;
    const h = document.documentElement.scrollHeight;
    const max = Math.max(h - vh, 1);        // 0除算を避ける保険
    return Math.min(y / max, 1);
  }
  window.addEventListener("scroll", () => {
    console.log("progress:", (getProgress() * 100).toFixed(1), "%");
  }, { passive: true });
</script>
HTML

ここが重要です:割合は「現在位置 ÷ 到達可能な最大量」。viewport 高さを引くのを忘れると 100% に達しません。0 で割らない保険も入れておくと安全です。

要素内の割合(水平・垂直どちらでも同じ考え方)

<script>
  function getElementProgress(el) {
    const top = el.scrollTop;
    const visible = el.clientHeight;
    const total = el.scrollHeight;
    const max = Math.max(total - visible, 1);
    return Math.min(top / max, 1);
  }
</script>
HTML

ここが重要です:水平の場合も scrollLeft / clientWidth / scrollWidth に読み替えるだけです。“見えている領域を引く”のがコアの考え方です。


パフォーマンスの基礎(passive / throttle / rAF)

監視の原則と最小コスト化

スクロールは毎フレーム級の頻度で発火するため、重い処理を直書きすると体感が悪化します。ここが重要です:基本は { passive: true }。重い処理は requestAnimationFrame(次の描画タイミングでまとめて実行)や throttle(一定間隔のみ実行)で間引きます。

requestAnimationFrame を使う例(描画タイミングでまとめる)

<script>
  let ticking = false;
  function onScrollFrame() {
    ticking = false;
    const y = window.scrollY;
    // ここで軽い描画更新(例えば進捗バーの幅更新)
  }
  window.addEventListener("scroll", () => {
    if (!ticking) {
      ticking = true;
      requestAnimationFrame(onScrollFrame);
    }
  }, { passive: true });
</script>
HTML

ここが重要です:フラグで1フレームに1回だけ更新。計算・描画を“同期”するとチラつきが減ります。

throttle の簡易例(一定間隔でのみ処理)

<script>
  let last = 0;
  const wait = 100; // ms
  window.addEventListener("scroll", () => {
    const now = performance.now();
    if (now - last < wait) return;
    last = now;
    // ここで軽い処理
  }, { passive: true });
</script>
HTML

ここが重要です:入力遅延の許容範囲に応じて wait を調整します。UI の体感に合わせた間引きが鍵です。


代表的な実装例(ヘッダの影、進捗バー、無限スクロール)

ヘッダの影をスクロール開始時に付ける

<header id="head" style="position:sticky; top:0; background:white">ヘッダ</header>
<script>
  function syncShadow() {
    head.classList.toggle("shadow", window.scrollY > 0);
  }
  window.addEventListener("scroll", syncShadow, { passive: true });
  syncShadow();
</script>
HTML

ここが重要です:スクロールが 0 より大きいかどうかでクラスを切り替えるだけ。軽い分岐と最小のスタイル変更に留めます。

ページ上部に進捗バーを表示

<div id="bar" style="position:fixed;top:0;left:0;height:3px;background:#09f;width:0"></div>
<script>
  function updateBar() {
    const h = document.documentElement.scrollHeight;
    const vh = window.innerHeight;
    const max = Math.max(h - vh, 1);
    const w = Math.min(window.scrollY / max, 1) * 100;
    bar.style.width = w + "%";
  }
  window.addEventListener("scroll", () => requestAnimationFrame(updateBar), { passive: true });
  updateBar();
</script>
HTML

ここが重要です:計算は最小限、反映は rAF 内で一発。進捗の視認性が高く、コストが低い作りです。

無限スクロール(下端近くで追加読み込み)

<ul id="list"></ul>
<script>
  let loading = false;
  function nearBottom() {
    const h = document.documentElement.scrollHeight;
    const vh = window.innerHeight;
    const y = window.scrollY;
    return y + vh > h - 200; // 下端200px手前
  }
  async function loadMore() {
    if (loading || !nearBottom()) return;
    loading = true;
    await new Promise(r => setTimeout(r, 400)); // 代わりに fetch など
    const frag = document.createDocumentFragment();
    for (let i = 0; i < 20; i++) {
      const li = document.createElement("li");
      li.textContent = "項目";
      frag.appendChild(li);
    }
    list.appendChild(frag);
    loading = false;
  }
  window.addEventListener("scroll", () => {
    if (!loading && nearBottom()) loadMore();
  }, { passive: true });
</script>
HTML

ここが重要です:境界の“ニアミス”で反応し、フラグで重複ロードを防ぎます。大量追加は DocumentFragment で一括にすると軽くなります。


IntersectionObserver の活用(スクロール監視の代替)

ビューポートに入ったら処理する例

<div id="sentinel"></div>
<script>
  const io = new IntersectionObserver((entries) => {
    entries.forEach((en) => {
      if (en.isIntersecting) {
        console.log("見えたら処理");
        // 画像遅延ロードや追加読み込みなど
      }
    });
  });
  io.observe(document.getElementById("sentinel"));
</script>
HTML

ここが重要です:スクロールイベントを自前で処理せず、ブラウザが“見えた・消えた”を通知してくれる仕組みを使うと、コードが短く性能も安定します。無限スクロールの「境界検知」には特に有効です。


よくある落とし穴と注意点

スクロール処理に重い計算や DOM 書き換えを大量に入れると体感が悪化します。まず passive を付け、rAF や throttle で間引き、描画更新はクラス切り替えなど最小限に抑えます。スクロール抑止(preventDefault)は UX を損ねやすいので、モーダル中のロックなど必要最小限に限定します。位置計算のときは viewport(window.innerHeight)とコンテンツ(scrollHeight)を正しく区別し、割合の計算で“見えている領域を引く”のを忘れないようにします。スマホではタッチスクロールが主体で、ホイールや dblclick の体験が異なるため、タッチ向けの設計(スナップ、慣性など)も意識します。


まとめ

スクロールイベントは高頻度に発火するため、軽さが命です。window と要素で扱いを分け、位置は scrollY/scrollTop、寸法は innerHeight/clientHeight/scrollHeight で正確に取る。パフォーマンスは { passive: true } を基本にし、requestAnimationFrame や throttle で間引く。典型パターンは“影の付与”“進捗バー”“無限スクロール”。監視は IntersectionObserver を使うとシンプルで高速になる。これらを守れば、初心者でも滑らかで意図通りのスクロール連動 UI を安全に実装できます。

タイトルとURLをコピーしました