パフォーマンスを損なわないスクロールイベントの処理方法

スクロールイベントは1秒間に数十回発生します。適切に処理しなければ、サイトのパフォーマンスが大幅に低下し、モバイル端末のバッテリーを消耗させ、ユーザー体験を損なうことになります。ここでは、スロットリング、デバウンス、パッシブリスナーを使用してスクロールハンドラーを最適化する方法を解説します。
重要なポイント
- スロットリングは一定間隔での関数実行を制限し、一貫した更新を提供します
- デバウンスはスクロールが停止するまで待機してから重い処理を実行します
- パッシブリスナーはブラウザの即座の最適化を可能にします
- Intersection Observerは可視性検出においてスクロールイベントを不要にします
問題:生のスクロールイベントがパフォーマンスを破壊する理由
スクロールの動きごとに複数のイベントが発生し、多くの場合1秒間に60回以上になります。これらのイベントに重い計算処理を紐づけると、ブラウザに以下の負荷をかけることになります:
- メインスレッドを繰り返しブロック
- スムーズなスクロールを妨害
- CPU使用率を劇的に増加
- モバイル端末のバッテリー寿命を短縮
// これはやってはいけません - 常に発火します
window.addEventListener('scroll', () => {
calculateExpensiveAnimation();
updateNavigationState();
checkElementVisibility();
});
解決策1:一貫した更新のためのスロットリング
スロットリングは関数の実行を固定間隔に制限します。定期的な更新が必要なスクロールベースのアニメーションやプログレスインジケーターに最適です。
function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
return func.apply(this, args);
}
};
}
// 最大100msごとに発火
window.addEventListener('scroll', throttle(() => {
updateScrollProgress();
}, 100));
スロットリングを使用する場面:
- スクロールプログレスバー
- パララックス効果
- ナビゲーション状態の更新
- リアルタイム位置追跡
解決策2:最終値のためのデバウンス
デバウンスはスクロールが停止するまで待機してから実行します。最終的なスクロール位置のみが必要な重い処理に理想的です。
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// スクロール停止から200ms後に発火
window.addEventListener('scroll', debounce(() => {
saveScrollPosition();
loadMoreContent();
}, 200));
デバウンスを使用する場面:
- 無限スクロールのトリガー
- 分析データの追跡
- スクロール位置の保存
- 重いDOM計算
Discover how at OpenReplay.com.
解決策3:即座のパフォーマンス向上のためのパッシブリスナー
パッシブリスナーはpreventDefault()
を呼び出さないことをブラウザに伝え、即座のスクロール最適化を可能にします。
// ブラウザは即座にスクロールを最適化できます
window.addEventListener('scroll', handleScroll, { passive: true });
このシンプルなフラグにより、ブラウザがデフォルト動作を防ぐかどうかをチェックする必要がなくなり、スクロールパフォーマンスが向上します。特にモバイルブラウザでこの最適化の恩恵を受けられます。
最大のパフォーマンスを得るための技術の組み合わせ
複雑なスクロールインタラクションには、複数のアプローチを組み合わせます:
// パッシブリスナー付きスロットリングハンドラー
const optimizedScroll = throttle(() => {
requestAnimationFrame(() => {
updateUI();
});
}, 16); // 約60fps
window.addEventListener('scroll', optimizedScroll, { passive: true });
モダンな代替手段:Intersection Observer
可視性検出には、スクロールイベントを完全に回避します:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
lazyLoadImage(entry.target);
}
});
});
observer.observe(document.querySelector('.lazy-image'));
Intersection Observerはスクロールリスナーなしで可視性検出を処理し、遅延読み込みやスクロールトリガーアニメーションにおいて優れたパフォーマンスを提供します。
迅速な判断ガイド
技術 | 使用場面 | 更新頻度 |
---|---|---|
スロットリング | プログレスバー、パララックス | 固定間隔 |
デバウンス | 状態保存、コンテンツ読み込み | スクロール停止後 |
パッシブ | あらゆるスクロールハンドラー | 常に(可能な場合) |
Intersection Observer | 可視性検出 | スクロールイベントなし |
実装のヒント
preventDefault()
が不要な場合は常にパッシブリスナーを使用- 60fpsアニメーションには16msのスロットリング遅延から開始
- ユーザートリガーアクションには200-300msのデバウンス遅延を使用
- 実戦テスト済みの実装にはLodashを検討
- 実際のパフォーマンス向上を測定するためにChrome DevToolsでプロファイリング
まとめ
最適化されていないスクロールハンドラーはパフォーマンスキラーです。スロットリングはアニメーション用の制御された更新を提供し、デバウンスは最終値を効率的に処理し、パッシブリスナーは即座のブラウザ最適化を提供します。可視性検出には、Intersection Observerでスクロールイベントを完全に回避しましょう。使用場面に適した技術を選択すれば、ユーザーはバッテリー寿命の向上という形で恩恵を受けるでしょう。
よくある質問
スロットリングはスクロール中に100msごとなど定期的な間隔で関数を実行します。デバウンスはスクロールが完全に停止するまで待機してから一度だけ実行します。継続的な更新にはスロットリングを、最終値には デバウンスを使用してください。
いいえ、パッシブリスナーはpreventDefaultを呼び出さないことを明示的にブラウザに伝えます。デフォルトのスクロール動作を防ぐ必要がある場合は、passiveをfalseに設定しますが、これはパフォーマンスに影響します。まず代替アプローチを検討してください。
遅延読み込み、無限スクロールトリガー、スクロール時のアニメーションなど、可視性ベースのロジックにはIntersection Observerを使用してください。スクロールイベントよりもパフォーマンスが良く、手動での位置チェックなしでビューポート計算を自動的に処理します。
Gain control over your UX
See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.