--- id: wiki-2026-0508-throttling-debouncing title: Throttling Debouncing category: 10_Wiki/Topics status: verified canonical_id: self aliases: [Throttle, Debounce, Rate Limiting Frontend] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [frontend, performance, event-handling, javascript] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: typescript framework: vanilla --- # Throttling Debouncing ## 매 한 줄 > **"매 throttle 의 rate-limit, debounce 의 quiet-period wait"**. 두 기법 모두 high-frequency event (scroll/resize/input/keypress) 의 handler call 빈도를 줄이지만, semantics 가 정반대 — throttle 의 every N ms 1 call, debounce 의 last call after N ms idle. ## 매 핵심 ### 매 Throttle - **Semantic**: max 1 call per interval. - **Use**: scroll, resize, mousemove — continuous events 의 sampling. - **Variants**: leading (immediate first), trailing (last in window), both. - **Visualizable**: `XX_XX_XX_XX_X` (regular ticks). ### 매 Debounce - **Semantic**: 1 call after last invocation + delay (no calls during burst). - **Use**: search input, form validate, autocomplete, window-resize-finish. - **Variants**: leading (immediate, then ignore), trailing (default), maxWait (force flush). - **Visualizable**: `XXX____X` (settles after burst). ### 매 응용 1. Search-as-you-type: debounce 300ms. 2. Infinite scroll trigger: throttle 100ms. 3. Auto-save: debounce 1s with maxWait 10s. ## 💻 패턴 ### Basic debounce (trailing) ```typescript function debounce void>( fn: T, ms: number ): (...args: Parameters) => void { let t: ReturnType | null = null; return (...args) => { if (t) clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; } // Usage const onSearch = debounce((q: string) => fetchResults(q), 300); input.addEventListener('input', e => onSearch((e.target as HTMLInputElement).value)); ``` ### Basic throttle (leading + trailing) ```typescript function throttle void>( fn: T, ms: number ): (...args: Parameters) => void { let last = 0; let t: ReturnType | null = null; let lastArgs: Parameters | null = null; return (...args) => { const now = Date.now(); const remaining = ms - (now - last); lastArgs = args; if (remaining <= 0) { if (t) { clearTimeout(t); t = null; } last = now; fn(...args); } else if (!t) { t = setTimeout(() => { last = Date.now(); t = null; if (lastArgs) fn(...lastArgs); }, remaining); } }; } ``` ### Debounce with cancel + flush ```typescript function debounceAdv void>(fn: T, ms: number) { let t: ReturnType | null = null; let lastArgs: Parameters | null = null; const debounced = (...args: Parameters) => { lastArgs = args; if (t) clearTimeout(t); t = setTimeout(() => { fn(...lastArgs!); t = null; }, ms); }; debounced.cancel = () => { if (t) { clearTimeout(t); t = null; } }; debounced.flush = () => { if (t && lastArgs) { clearTimeout(t); fn(...lastArgs); t = null; } }; return debounced; } ``` ### React hook: useDebouncedValue ```typescript import { useEffect, useState } from 'react'; export function useDebouncedValue(value: T, ms = 300): T { const [v, setV] = useState(value); useEffect(() => { const t = setTimeout(() => setV(value), ms); return () => clearTimeout(t); }, [value, ms]); return v; } // Usage function Search() { const [q, setQ] = useState(''); const debounced = useDebouncedValue(q, 300); useEffect(() => { if (debounced) fetchResults(debounced); }, [debounced]); return setQ(e.target.value)} />; } ``` ### rAF throttle (60fps cap) ```typescript function rafThrottle void>(fn: T) { let scheduled = false; let lastArgs: Parameters; return (...args: Parameters) => { lastArgs = args; if (scheduled) return; scheduled = true; requestAnimationFrame(() => { fn(...lastArgs); scheduled = false; }); }; } // Best for scroll/resize on 60-120hz displays window.addEventListener('scroll', rafThrottle(() => updatePosition())); ``` ### lodash equivalents ```typescript import { debounce, throttle } from 'lodash-es'; const onResize = throttle(handleResize, 200, { leading: true, trailing: true }); const onSearch = debounce(handleSearch, 300, { maxWait: 1000 }); // Cleanup on unmount useEffect(() => () => { onResize.cancel(); onSearch.cancel(); }, []); ``` ## 매 결정 기준 | 상황 | Approach | |---|---| | Search input | debounce 200-400ms | | Scroll handler (positional) | rAF throttle | | Resize end detection | debounce 150ms | | Auto-save | debounce 1s + maxWait 10s | | Rate-limited API call | throttle (leading) | | Button double-click guard | debounce leading-only | **기본값**: lodash `debounce`/`throttle` for non-trivial cases; rAF throttle for animation-tied work; useDebouncedValue hook for React inputs. ## 🔗 Graph - 부모: [[Event Handling]] · [[Frontend Performance]] - 변형: [[Rate Limiting]] · [[Backpressure]] - 응용: [[Infinite Scroll]] - Adjacent: [[requestAnimationFrame]] · [[Web Worker (웹 워커)|Web Workers]] · [[Reactive Streams]] ## 🤖 LLM 활용 **언제**: input handler optimization, scroll/resize perf, rate-limiting UI events. **언제 X**: server-side rate limiting (use token bucket / Redis), animation timing (use rAF directly). ## ❌ 안티패턴 - **Debounce on click button**: user feels lag; use throttle leading-only. - **No cleanup on unmount**: timer fires on unmounted component → setState warning / leak. - **Throttle 16ms manually**: use rAF throttle — sync to display refresh. - **Debounce without maxWait on auto-save**: user types continuously → never saves. - **Inline `() => debounce(fn, 300)` in render**: new function each render, debouncing breaks. ## 🧪 검증 / 중복 - Verified (lodash docs, MDN event handling, React docs useDeferredValue/useTransition equivalence). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — full canonical (debounce/throttle/rAF/React hook + lodash) |