ヘッダーの表示切替、ページの読了率表示、要素のフェードインアニメーションなど、スクロールに応じて処理を実行する機能は Web サイトの定番です。JavaScript の scroll イベントで実現できますが、パフォーマンスへの影響が大きいため正しい使い方を理解しておく必要があります。
この記事でわかること
・addEventListener でスクロールイベントを設定する方法
・window.scrollY でスクロール量を取得する方法
・throttle / requestAnimationFrame によるパフォーマンス最適化
・IntersectionObserver でスクロールイベントを使わずに要素を検出する方法
・スクロール方向(上下)を検出する方法
・スクロール率(ページの何%まで読んだか)の計算
・passive オプション
・ヘッダー切替・プログレスバー・フェードインの実務パターン
・addEventListener でスクロールイベントを設定する方法
・window.scrollY でスクロール量を取得する方法
・throttle / requestAnimationFrame によるパフォーマンス最適化
・IntersectionObserver でスクロールイベントを使わずに要素を検出する方法
・スクロール方向(上下)を検出する方法
・スクロール率(ページの何%まで読んだか)の計算
・passive オプション
・ヘッダー切替・プログレスバー・フェードインの実務パターン
スクロールイベントの基本
JavaScript
window.addEventListener("scroll", () => {
console.log("スクロール中:", window.scrollY);
});
scroll イベントはスクロール中に毎秒数十回〜数百回発火します。コールバック内の処理が重いとページがカクつくため、軽い処理に留めるか、後述の最適化テクニックを適用してください。
スクロール量を取得するプロパティ
| プロパティ | 取得する値 | 対象 |
|---|---|---|
window.scrollY |
ページ全体の縦スクロール量(px) | ウィンドウ |
window.scrollX |
ページ全体の横スクロール量(px) | ウィンドウ |
element.scrollTop |
要素内の縦スクロール量(px) | 特定の要素 |
element.scrollLeft |
要素内の横スクロール量(px) | 特定の要素 |
特定の要素のスクロールを検出
const container = document.getElementById("scrollContainer");
container.addEventListener("scroll", () => {
console.log("要素内スクロール:", container.scrollTop);
});
window.pageYOffset は window.scrollY の別名(エイリアス)です。モダンブラウザでは scrollY を使いましょう。パフォーマンスを最適化する
スクロールイベントの発火頻度は非常に高いため、DOM 操作や計算を毎回行うとパフォーマンスが低下します。3 つの最適化手法を紹介します。
throttle(一定間隔に間引く)
JavaScript
function throttle(fn, interval) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
// 100ms ごとに1回だけ実行
window.addEventListener("scroll", throttle(() => {
console.log("throttled:", window.scrollY);
}, 100));
requestAnimationFrame(描画タイミングに合わせる)
JavaScript
let ticking = false;
window.addEventListener("scroll", () => {
if (!ticking) {
requestAnimationFrame(() => {
handleScroll(window.scrollY);
ticking = false;
});
ticking = true;
}
});
function handleScroll(scrollY) {
// 1フレームに1回だけ実行される
console.log("rAF:", scrollY);
}
passive オプション
{ passive: true } を指定すると、ブラウザに「このリスナーは preventDefault を呼ばない」と宣言します。ブラウザはスクロールのブロッキングを心配せずにスクロールを即座に開始でき、操作の滑らかさが向上します。
JavaScript
window.addEventListener("scroll", handleScroll, { passive: true });
| 最適化手法 | 特徴 | 推奨場面 |
|---|---|---|
| throttle | 一定間隔で実行を間引く | ヘッダー表示切替・スクロール量の判定 |
| requestAnimationFrame | 描画のタイミングに合わせて実行 | アニメーション・スタイル変更 |
| passive | スクロールのブロッキングを防止 | すべてのスクロールリスナー |
passive: true のリスナー内で
preventDefault() を呼ぶと無視されます。スクロールを止める必要がある場合(モーダル表示中など)は passive を使わないでください。IntersectionObserver でスクロールイベントを使わずに検出する
「特定の要素が画面内に入ったか」を検出する場合、scroll イベントよりも IntersectionObserver の方がパフォーマンスが良く、コードもシンプルです。
JavaScript
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
observer.unobserve(entry.target); // 一度だけ実行
}
});
}, {
threshold: 0.1 // 10% 見えたら発火
});
// 監視対象の要素を登録
document.querySelectorAll(".fade-in").forEach(el => {
observer.observe(el);
});
| 比較項目 | scroll イベント | IntersectionObserver |
|---|---|---|
| 発火タイミング | スクロールのたびに毎回 | 要素がビューポートに出入りしたときだけ |
| パフォーマンス | throttle が必要 | ブラウザが最適化 |
| コードの簡潔さ | getBoundingClientRect で位置計算が必要 | isIntersecting で判定するだけ |
| 用途 | スクロール量・方向・率の計算 | 要素の表示検出・遅延読み込み |
「要素が画面に入ったらアニメーション」のようなユースケースでは、scroll イベントではなく
IntersectionObserver を使いましょう。スクロール方向(上下)を検出する
前回のスクロール位置と比較することで、スクロール方向を判定できます。
JavaScript
let lastScrollY = 0;
window.addEventListener("scroll", throttle(() => {
const currentY = window.scrollY;
if (currentY > lastScrollY) {
console.log("下にスクロール");
} else if (currentY < lastScrollY) {
console.log("上にスクロール");
}
lastScrollY = currentY;
}, 100));
スクロール率(読了率)を計算する
JavaScript
function getScrollPercentage() {
const scrollY = window.scrollY;
const docHeight = document.documentElement.scrollHeight;
const winHeight = window.innerHeight;
return Math.round((scrollY / (docHeight - winHeight)) * 100);
}
window.addEventListener("scroll", throttle(() => {
console.log(getScrollPercentage() + "%");
}, 100));
| プロパティ | 意味 |
|---|---|
window.scrollY |
現在のスクロール量(px) |
document.documentElement.scrollHeight |
ページ全体の高さ(px) |
window.innerHeight |
ビューポートの高さ(px) |
実務でよく使うパターン
スクロールでヘッダーを表示 / 非表示にする
JavaScript
const header = document.querySelector("header");
let lastScrollY = 0;
window.addEventListener("scroll", throttle(() => {
const currentY = window.scrollY;
if (currentY > lastScrollY && currentY > 100) {
// 下スクロール → ヘッダーを隠す
header.classList.add("header-hidden");
} else {
// 上スクロール → ヘッダーを表示
header.classList.remove("header-hidden");
}
lastScrollY = currentY;
}, 100), { passive: true });
読了プログレスバー
HTML
<div id="progress-bar" style="position:fixed;top:0;left:0;height:3px;background:#0284c7;width:0;z-index:9999;transition:width 0.1s;"></div>
JavaScript
const bar = document.getElementById("progress-bar");
window.addEventListener("scroll", () => {
const pct = getScrollPercentage();
bar.style.width = pct + "%";
}, { passive: true });
スクロールで要素をフェードイン(IntersectionObserver)
CSS
.fade-in {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.fade-in.visible {
opacity: 1;
transform: translateY(0);
}
JavaScript
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
}
});
}, { threshold: 0.15 });
document.querySelectorAll(".fade-in").forEach(el => observer.observe(el));
関連記事
- 上向きスクロールで表示されるヘッダーの作り方
- ページ上部へ戻るボタンの実装方法
- スムーススクロールを実装する方法
- IntersectionObserver を使って複数要素にイベントを設定する方法
- IntersectionObserver で要素をふわっと表示させる方法
- クリックイベントの設定方法
よくある質問
Qスクロールイベントのパフォーマンスが悪いです。
Ascroll イベントは毎秒数十〜数百回発火します。
throttle(100ms 間隔に間引き)や requestAnimationFrame(描画タイミングに同期)で実行回数を制限してください。また、{ passive: true } オプションでスクロールのブロッキングを防止しましょう。Qscroll イベントと IntersectionObserver はどう使い分けますか?
A「要素が画面に入ったか」を検出するなら
IntersectionObserver(パフォーマンス良好、コードも簡潔)。スクロール量・スクロール方向・読了率など数値ベースの判定には scroll イベントが必要です。Qスクロール方向を検出するにはどうすればよいですか?
A前回のスクロール位置を変数に保存しておき、現在の
window.scrollY と比較します。currentY > lastScrollY なら下方向、currentY < lastScrollY なら上方向です。Qpassive: true とは何ですか?
Aブラウザに「このリスナーは
preventDefault を呼ばない」と宣言するオプションです。ブラウザはスクロールの開始を遅らせずに済むため、操作が滑らかになります。スクロール・タッチ系のリスナーには原則 passive: true を付けましょう。Qページの読了率(何%スクロールしたか)を計算するには?
A
Math.round(scrollY / (scrollHeight - innerHeight) * 100) で 0〜100% の値が得られます。scrollHeight はページ全体の高さ、innerHeight はビューポートの高さです。まとめ
JavaScript でスクロールイベントを使う方法を整理しました。
- 基本:
addEventListener("scroll", fn)でスクロールを検出 - スクロール量:
window.scrollY(ページ)/el.scrollTop(要素) - 最適化: throttle / requestAnimationFrame / passive: true
- 要素の検出: IntersectionObserver が scroll イベントより高パフォーマンス
- 方向検出: 前回の scrollY と比較して上下を判定
- 読了率: scrollY / (scrollHeight - innerHeight) × 100
スクロールイベントは便利ですが、パフォーマンスへの影響が大きいことを常に意識しましょう。要素の表示検出は IntersectionObserver、数値ベースの判定は throttle 付きの scroll イベントと使い分けてください。