【JavaScript】スクロール・リサイズ時の処理負荷を軽減!DebounceとThrottleでイベント連発を制御し、Webパフォーマンスを改善する方法
※本ページのリンクにはプロモーションが含まれています。
こんにちは、Ryohei(@ityryohei)です!
本記事では、Webサイトのパフォーマンスを低下させる最大の原因の一つである、イベントの連発を制御する、モダンなJavaScriptのテクニック、Debounce(デバウンス)とThrottle(スロットル)を徹底解説します。
スクロール時のアニメーションがカクつく、リサイズ中にサイトが重くなる...。これらの処理負荷を効果的に軽減し、ユーザー体験を改善したいんだけど、どうすればいいんだろう?
上記の疑問にお答えします。これらのテクニックを使うことで、Webサイトのパフォーマンスを劇的に改善できます。
では、解説していきます。イベントの連発が引き起こす深刻な問題
Webブラウザ上で動作するJavaScriptは、基本的にシングルスレッド(一つの処理の流れ)で動作しています。そのため、以下のイベントが連続して発火すると、その処理がメインスレッドを占有し、ブラウザのレンダリング(描画)処理を妨害してしまいます。
| イベントの種類 | 連発しやすい状況 | 問題点 |
scroll | ユーザーがマウスホイールやトラックパッドを高速に操作しているとき | スクロール追従、アニメーション、コンテンツの遅延読み込みがカクつく。 |
resize | ウィンドウの端をドラッグしてサイズ変更しているとき | レイアウトの再計算(リフロー)が頻繁に発生し、CPU負荷が急増する。 |
input | 検索フォームなどで、文字を高速にタイピングしているとき | サーバーへの無駄なAPIリクエストが大量に発生する。 |
この問題は、特にモバイル環境や低スペックPCで顕著です。この「無駄なイベント実行」を制御するために、DebounceとThrottleが登場します。
Debounce(デバウンス): 「終了後」に一度だけ実行
Debounceは、「連続する操作が完全に止まった後、一定の時間が経過してから、最後に一度だけ」処理を実行する仕組みです。タイマーをリセットし続けることで、連続実行を防ぎます。
Debounceの実装と動作原理
Debounce関数は、内部でsetTimeoutのIDを保持し、関数が呼び出されるたびに前回のタイマーを強制的にキャンセル(clearTimeout)します。これにより、指定時間内に次の操作が行われた場合、処理は永久に待機し、操作が途切れた後の最後の1回だけ実行が保証されます。
JavaScript
// Debounce 関数
const debounce = (func, delay) => {
let timeoutId; // タイマーIDを保持する変数 (クロージャで保持される)
// 処理実行のための関数を返す
return function() {
// 呼び出されるたびに前回のタイマーをクリア!
clearTimeout(timeoutId);
// delayミリ秒後にfuncを実行する新しいタイマーを設定
timeoutId = setTimeout(() => {
// applyを使って、呼び出し元のthisと引数を渡す
func.apply(this, arguments);
}, delay);
};
};
Debounceが最適なユースケース
Debounceは、「操作の途中経過は不要で、最終結果だけがほしい」場合に最適です。
| ユースケース | 実行タイミング |
| 検索サジェスト | タイピングが完全に止まった後 (例: 500ms後) |
| フォームのバリデーション | 入力が完了し、フォーカスが外れる前 (例: 300ms後) |
| ウィンドウリサイズ | リサイズ操作が完全に終わった後 |
Throttle(スロットル): 「間隔」を空けて定期的に実行
Throttleは、「一定の時間間隔(インターバル)ごとに」処理を実行する仕組みです。操作が連続していても、処理の頻度をコントロールし、間引き(間隔を空ける)ことで負荷を抑えます。
Throttleの実装と動作原理
Throttle関数は、内部で実行制限中のフラグ(inThrottle)を保持します。処理を実行した後、フラグを立ててタイマーが解除されるまで次の実行をブロックします。これにより、処理は一定のレート(頻度)で実行されます。
JavaScript
// Throttle 関数
const throttle = (func, limit) => {
let inThrottle; // 実行制限中のフラグ
// 処理実行のための関数を返す
return function() {
const context = this;
const args = arguments;
// inThrottleが false の場合のみ実行を許可!
if (!inThrottle) {
// 実行
func.apply(context, args);
inThrottle = true; // フラグを立てる
// limitミリ秒後に実行フラグを解除
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
};
Throttleが最適なユースケース
Throttleは、「操作中に定期的なフィードバックが必要」な場合に最適です。
| ユースケース | 実行間隔 |
| スクロールアニメーション | 描画頻度に合わせて (例: 100ms〜300ms間隔) |
| 追従型ヘッダーの表示/非表示 | スクロール中に定期的にチェックしたい場合 |
| 要素の可視性チェック | スクロールに合わせて要素が画面内に入ったかを確認する場合 |
実践!コード全体とデモ
ここでは、DebounceとThrottleを実際にDOM操作に組み込むデモコード全体を示します。
<html>
<head>
<meta charset="utf-8">
<title>Debounce & Throttle パフォーマンス改善デモ</title>
<style>
body {
height: 300vh; /* スクロール可能にする */
margin: 0;
padding: 50px;
font-family: sans-serif;
}
#search-input {
width: 80%;
padding: 10px;
font-size: 1.2em;
border: 2px solid #00c2bc;
margin-bottom: 20px;
}
.result-box {
background-color: #f0f8ff;
padding: 15px;
border-radius: 5px;
border-left: 5px solid #00c2bc;
}
h2 {
color: #00c2bc;
}
.scroll-indicator {
position: fixed;
top: 0;
right: 0;
padding: 10px;
background: #ff5722;
color: white;
font-weight: bold;
z-index: 1000;
}
</style>
</head>
<body>
<h1>イベント制御のデモ</h1>
<p class="result-box">開発者ツールのコンソール (F12) を開いて、イベントの実行回数の違いを確認しながら操作してみてください。</p>
<div id="scroll-indicator" class="scroll-indicator">Scroll Y: 0</div>
<h2>Debounceデモ(500ms)</h2>
<p>文字を入力してみてください。入力が止まって0.5秒後に下のメッセージが更新されます。</p>
<input type="text" id="search-input" placeholder="ここに入力してください...">
<div id="debounce-output" style="color: #ff5722; font-weight: bold;"></div>
<h2>Throttleデモ(200ms)</h2>
<p>下にスクロールしてください。コンソールでイベントの間隔が空いていることを確認できます。</p>
<div style="height: 150vh; background: #eee; padding: 20px;">
<p>コンテンツを下にスクロール...</p>
</div>
<script>
// ===================================
// Debounce 実装
// ===================================
const debounce = (func, delay) => {
let timeoutId;
return function() {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, arguments);
}, delay);
};
};
// ===================================
// Throttle 実装
// ===================================
const throttle = (func, limit) => {
let inThrottle;
return function() {
const context = this;
const args = arguments;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
};
// ===================================
// 処理の定義と適用
// ===================================
// 1. Debounceを適用した処理 (検索入力)
const debounceOutput = document.getElementById('debounce-output');
let debounceCounter = 0;
const handleInput = (e) => {
debounceCounter++;
const value = e.target.value;
const msg = `デバウンス処理実行! (回数: ${debounceCounter}) 最新の値: ${value}`;
console.log("DB:", msg);
debounceOutput.textContent = msg;
};
document.getElementById('search-input').addEventListener('input', debounce(handleInput, 500));
// 2. Throttleを適用した処理 (スクロール追跡)
const scrollIndicator = document.getElementById('scroll-indicator');
let throttleCounter = 0;
const handleScroll = () => {
throttleCounter++;
const scrollY = window.scrollY;
// 頻繁に実行されるが、間隔が制限されていることを確認
console.log(`THROTTLE: 処理実行 (${throttleCounter}回目), Y軸: ${scrollY}`);
// 追従インジケーターの更新(デモ用)
scrollIndicator.textContent = `Scroll Y: ${Math.floor(scrollY)}`;
};
// 実際のイベントリスナーにThrottle化した関数を登録(200ms間隔)
window.addEventListener('scroll', throttle(handleScroll, 200));
</script>
</body>
</html>
実行結果
See the Pen 8255 by ryohei (@intotheprogram) on CodePen.
さらなる最適化!RequestAnimationFrame(rAF)の活用
スクロールやマウス移動など、視覚的な滑らかさが求められる処理のパフォーマンスを極限まで高めたい場合は、Throttleと組み合わせてrequestAnimationFrame (rAF)を利用することが推奨されます。
rAFは、ブラウザの描画サイクル(通常は16.7msごと)に合わせて処理を実行するようスケジューリングするAPIです。これにより、CSSアニメーションなどと処理のタイミングが同期し、よりスムーズな動作を実現できます。
rAFを活用したThrottleの改良版
JavaScript
// rAFを利用したハイブリッドThrottle
const rafThrottle = (func) => {
let ticking = false; // 描画待ちフラグ
return function() {
const context = this;
const args = arguments;
// すでに次の描画サイクルで実行待ちであれば何もしない
if (!ticking) {
ticking = true;
// 次のブラウザ描画タイミングで実行するよう予約
requestAnimationFrame(() => {
func.apply(context, args);
ticking = false; // 実行完了後、フラグを解除
});
}
};
};
// 使用例:ブラウザの描画タイミングに合わせてスクロール処理を実行
window.addEventListener('scroll', rafThrottle(handleScroll));
このrafThrottleを使用することで、ブラウザが処理可能な最適な頻度(毎フレーム)でのみ処理を行するようになり、特にスムーズなアニメーションやUIの更新が必要な場面で最高のパフォーマンスを発揮します。
DebounceとThrottle、そしてrAFを使いこなし、ユーザーにストレスのない快適なWebサイトを提供しましょう。
以上、JavaScriptでスクロール・リサイズ時の処理負荷を軽減!DebounceとThrottleでイベント連発を制御し、Webパフォーマンスを改善する方法のご紹介でした!