Перейти к содержимому

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 вызывает функцию только после того, как прошло 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мс
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() вызовется снова
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 гарантирует, что функция вызовется не чаще одного раза в 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)
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)
}
}
}
DebounceThrottle
Поиск/автодополнение
Автосохранение
Scroll/resize
Прогресс-бар загрузки
Кнопка «Отправить»
Mousemove/координаты
function rafThrottle(fn) {
let scheduled = false
return function(...args) {
if (!scheduled) {
scheduled = true
requestAnimationFrame(() => {
fn.apply(this, args)
scheduled = false
})
}
}
}
// Идеально для анимаций и обновления DOM
const updatePosition = rafThrottle((x, y) => {
cursor.style.transform = `translate(${x}px, ${y}px)`
})
document.addEventListener('mousemove', (e) => {
updatePosition(e.clientX, e.clientY)
})