JavaScriptの debounce / throttle を理解する|なぜ“連打”で壊れるのか?

こんにちは、さるまりんです 🐒🔧

JavaScriptを書いていると、

  • 検索フォーム
  • スクロール処理
  • resizeイベント
  • ボタン連打防止

などで、

「イベントが何回も呼ばれて困る」

という場面に出会うことがあります。

そこでよく出てくるのが、

  • debounce
  • throttle

です。

でも、

  • 聞いたことあるんだけど違いが曖昧
  • とりあえずコピペしている
  • AIに生成してもらったけどよくわかっていない

という状態になりやすい部分でもあります。

なので今回は、

「なぜ必要なのか?」

から、実際に動かしながら整理してみます。


■ まずは「壊してみる」

まずは debounce も throttle も使わず、
イベントをそのまま処理してみます。

以下を index.html として保存して、
ブラウザで開いてみます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>resize test</title>
</head>
<body>

<h1>resize test</h1>

<script>
window.addEventListener('resize', () => {
  console.log('resize');
});
</script>

</body>
</html>

ブラウザを開いた状態で、
ウィンドウサイズを少し動かしてみます。

すると…

開発者ツールで console を見てみると、

resize
resize
resize
resize
resize
resize
resize

ものすごい勢いで実行されます。

ほんの少し動かしただけでも、
大量にイベントが発生しています。


■ イベントは想像以上に大量発生する

ここがかなり重要です。

JavaScriptのイベントは、

「1回操作したら1回動く」

とは限りません。

例えば:

イベント 大量発生しやすい
scroll かなり多い
resize かなり多い
input タイピングごと
mousemove 非常に多い

つまり、

console.log('動いた');

程度なら問題なくても、

  • API通信
  • DB検索
  • 重い描画
  • ファイル処理

などを直接書くと、
一気に危険になります。


■ debounce とは?

debounce は、

「最後の1回だけ実行する」

仕組みです。

例えば検索フォーム。

ユーザーが:

c
ca
cat

と入力した時に、
毎回API通信すると無駄が多くなります。

本当に欲しいのは、

cat

まで入力が終わった後ですよね。

そこで debounce を使います。


■ debounce を試してみる

以下を試してみます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>debounce test</title>
</head>
<body>

<input type="text" id="search" placeholder="入力してください">

<script>
function debounce(func, wait) {
  let timeout;

  return function (...args) {
    clearTimeout(timeout);

    timeout = setTimeout(() => {
      func.apply(this, args);
    }, wait);
  };
}

const searchInput = document.getElementById('search');

searchInput.addEventListener(
  'input',
  debounce((event) => {
    console.log('検索:', event.target.value);
  }, 500)
);
</script>

</body>
</html>

入力してみると、

タイプ中は動かず、
少し止まったタイミングで実行されます。

つまり:

入力中 → 待つ → 実行

です。


■ debounce が向いているケース

例えば:

用途 相性
検索フォーム
オートコンプリート
API検索
入力補助

逆に、

「リアルタイム性」

が欲しい場合には向きません。


■ throttle とは?

throttle は debounce と少し違います。

こちらは、

「一定間隔でだけ実行する」

仕組みです。

例えば scroll。

スクロール中ずっと止まるまで待っていたら、
画面更新が遅れてしまいます。

なので:

100msごとに実行

のように、
回数を間引きます。


■ throttle を試してみる

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>throttle test</title>

  <style>
    body {
      height: 3000px;
    }
  </style>
</head>
<body>

<h1>scrollしてください</h1>

<script>
function throttle(func, limit) {
  let lastExecuted = 0;

  return function (...args) {
    const now = Date.now();

    if (now - lastExecuted >= limit) {
      lastExecuted = now;
      func.apply(this, args);
    }
  };
}

window.addEventListener(
  'scroll',
  throttle(() => {
    console.log('scroll');
  }, 300)
);
</script>

</body>
</html>

スクロールしてみると、
連続ではなく、
一定間隔でだけ実行されます。


■ debounce と throttle の違い

機能 debounce throttle
動き 最後だけ実行 間引きながら実行
向いている用途 検索 scroll
リアルタイム性 低い 高い
API保護 強い 中程度

■ 実務では「負荷」より「体験」が大事なこともある

この記事では負荷の話を多めにしましたが、
実際にはUXのために使うことも多いです。

例えば:

  • スクロールが重い
  • 入力中に画面がカクつく
  • ボタン連打で二重送信される

こういう問題は、

「サーバーが壊れる」

より先に、

「使いにくい」

として現れます。

つまり debounce / throttle は、

単なる最適化ではなく、

「気持ちよく動かすための設計」

でもあります。

どこにどんなテクニックを使うか、
とても大切ですよね。


■ AI時代だからこそ理解しておきたい

今は AI に、

debounce 関数を書いて

と頼めば、
簡単にコードを出してくれます。

でも、

  • なぜ必要なのか
  • debounce と throttle の違い
  • どちらを選ぶべきか

が分からないと、
本当に必要な場面で使えません。

逆に、

考え方を理解していると、
AIが生成したコードも読みやすくなります。

AIにどっちがいいか提案してもらうのもいいでしょうし、
違いをとことん説明してもらうこともできるかなと思います。

でも、やっぱり基本的なことを知っていることと、
何を実現したいかを知っているのは、
作っている本人ですよね。


■ まとめ

debounce と throttle は、
どちらも

「イベントをそのまま流さない」

ための仕組みです。

ただ、

  • 最後だけ実行したい → debounce
  • 一定間隔で実行したい → throttle

という違いがあります。

JavaScriptでは、
イベントが想像以上に大量発生します。

だからこそ、

「動くコード」

だけではなく、

「どう制御するか」

も大切になってきますね。

読んでくださってありがとうございました。
それではまた!