こんにちは、Ryohei(@ityryohei)です!

本記事では、Webサイトのパフォーマンスを低下させる最大の原因の一つである、イベントの連発を制御する、モダンなJavaScriptのテクニック、Debounce(デバウンス)とThrottle(スロットル)を徹底解説します。

スクロール時のアニメーションがカクつく、リサイズ中にサイトが重くなる...。これらの処理負荷を効果的に軽減し、ユーザー体験を改善したいんだけど、どうすればいいんだろう?

上記の疑問にお答えします。これらのテクニックを使うことで、Webサイトのパフォーマンスを劇的に改善できます。

では、解説していきます。

イベントの連発が引き起こす深刻な問題

Webブラウザ上で動作するJavaScriptは、基本的にシングルスレッド(一つの処理の流れ)で動作しています。そのため、以下のイベントが連続して発火すると、その処理がメインスレッドを占有し、ブラウザのレンダリング(描画)処理を妨害してしまいます。

イベントの種類連発しやすい状況問題点
scrollユーザーがマウスホイールやトラックパッドを高速に操作しているときスクロール追従、アニメーション、コンテンツの遅延読み込みがカクつく
resizeウィンドウの端をドラッグしてサイズ変更しているときレイアウトの再計算(リフロー)が頻繁に発生し、CPU負荷が急増する。
input検索フォームなどで、文字を高速にタイピングしているときサーバーへの無駄なAPIリクエストが大量に発生する。

この問題は、特にモバイル環境や低スペックPCで顕著です。この「無駄なイベント実行」を制御するために、DebounceThrottleが登場します。

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間隔)
追従型ヘッダーの表示/非表示スクロール中に定期的にチェックしたい場合
要素の可視性チェックスクロールに合わせて要素が画面内に入ったかを確認する場合

実践!コード全体とデモ

ここでは、DebounceThrottleを実際に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の更新が必要な場面で最高のパフォーマンスを発揮します。

DebounceThrottle、そしてrAFを使いこなし、ユーザーにストレスのない快適なWebサイトを提供しましょう。

以上、JavaScriptでスクロール・リサイズ時の処理負荷を軽減!DebounceとThrottleでイベント連発を制御し、Webパフォーマンスを改善する方法のご紹介でした!

この記事を書いた人

Ryohei

Webエンジニア / ブロガー

福岡のWeb制作会社に務めるWebエンジニアです。エンジニア歴は10年程で、好きな言語はPHPとJavaScriptです。本サイトは私がインプットしたWebに関する知識を整理し、共有することを目的に2015年から運営しています。Webに関するご相談があれば気軽にお問い合わせください。