91. Debounce и Throttle
Debounce и Throttle — техники ограничения частоты вызова функций. Незаменимы при работе с событиями, которые могут срабатывать сотни раз в секунду: scroll, resize, input, mousemove.
Проблема: слишком частые вызовы
Заголовок раздела «Проблема: слишком частые вызовы»// БЕЗ оптимизации — вызов API при каждом нажатии клавишиinput.addEventListener('input', (e) => { fetch(`/api/search?q=${e.target.value}`) // 💥 запрос на каждый символ!})
// Пользователь набирает "JavaScript" (10 символов) → 10 запросов к API// Нужно: 1 запрос после паузы в набореDebounce — «откладывает» вызов
Заголовок раздела «Debounce — «откладывает» вызов»Debounce вызывает функцию только после того, как прошло delay мс без новых вызовов. Идеален для поиска и автосохранения.
function debounce(fn, delay) { let timer
return function(...args) { clearTimeout(timer) // отменяем предыдущий таймер timer = setTimeout(() => { fn.apply(this, args) }, delay) }}
// Поиск — запрос отправится через 300мс после паузы в набореconst search = debounce((query) => { console.log(`Поиск: "${query}"`) // fetch(`/api/search?q=${query}`)}, 300)
search('J') // отменёнsearch('Ja') // отменёнsearch('Jav') // отменёнsearch('JavaScript') // выполнится через 300мсDebounce с immediate (leading edge)
Заголовок раздела «Debounce с immediate (leading edge)»function debounce(fn, delay, immediate = false) { let timer
return function(...args) { const callNow = immediate && !timer
clearTimeout(timer) timer = setTimeout(() => { timer = null if (!immediate) fn.apply(this, args) }, delay)
if (callNow) fn.apply(this, args) }}
// immediate=true: вызов немедленно, потом паузаconst saveNow = debounce(save, 1000, true)saveNow() // вызов немедленно ✅saveNow() // игнорируется (пауза)saveNow() // игнорируется// Через 1000мс после последнего → следующий saveNow() вызовется сноваDebounce с отменой
Заголовок раздела «Debounce с отменой»function debounce(fn, delay) { let timer
function debounced(...args) { clearTimeout(timer) return new Promise((resolve) => { timer = setTimeout(() => resolve(fn.apply(this, args)), delay) }) }
debounced.cancel = () => { clearTimeout(timer) timer = null }
debounced.flush = function(...args) { clearTimeout(timer) return fn.apply(this, args) }
return debounced}
const debouncedSave = debounce(save, 500)
// При размонтировании компонентаwindow.addEventListener('beforeunload', () => { debouncedSave.flush() // сохранить немедленно!})Throttle — «ограничивает» частоту
Заголовок раздела «Throttle — «ограничивает» частоту»Throttle гарантирует, что функция вызовется не чаще одного раза в delay мс. Идеален для scroll и resize.
function throttle(fn, delay) { let lastCall = 0
return function(...args) { const now = Date.now() if (now - lastCall >= delay) { lastCall = now return fn.apply(this, args) } }}
// Обновляем позицию скролла максимум 10 раз в секунду (каждые 100мс)const handleScroll = throttle(() => { console.log('Скролл:', window.scrollY)}, 100)
window.addEventListener('scroll', handleScroll)Throttle с trailing call (по таймеру)
Заголовок раздела «Throttle с trailing call (по таймеру)»function throttle(fn, delay) { let lastCall = 0 let timer = null
return function(...args) { const now = Date.now() const remaining = delay - (now - lastCall)
if (remaining <= 0) { if (timer) { clearTimeout(timer); timer = null } lastCall = now fn.apply(this, args) } else if (!timer) { // Гарантируем последний вызов timer = setTimeout(() => { lastCall = Date.now() timer = null fn.apply(this, args) }, remaining) } }}Debounce vs Throttle: когда что
Заголовок раздела «Debounce vs Throttle: когда что»| Debounce | Throttle | |
|---|---|---|
| Поиск/автодополнение | ✅ | ❌ |
| Автосохранение | ✅ | ❌ |
| Scroll/resize | ❌ | ✅ |
| Прогресс-бар загрузки | ❌ | ✅ |
| Кнопка «Отправить» | ✅ | ✅ |
| Mousemove/координаты | ❌ | ✅ |
Практический паттерн: requestAnimationFrame throttle
Заголовок раздела «Практический паттерн: requestAnimationFrame throttle»function rafThrottle(fn) { let scheduled = false
return function(...args) { if (!scheduled) { scheduled = true requestAnimationFrame(() => { fn.apply(this, args) scheduled = false }) } }}
// Идеально для анимаций и обновления DOMconst updatePosition = rafThrottle((x, y) => { cursor.style.transform = `translate(${x}px, ${y}px)`})
document.addEventListener('mousemove', (e) => { updatePosition(e.clientX, e.clientY)})