Пошук уроків, статей та іншого контенту
Дві техніки обмеження частоти викликів функції — і коли застосовувати кожну.
Деякі події браузера спрацьовують набагато частіше, ніж потрібно реагувати: scroll і resize можуть викликатись десятки разів за секунду, input — на кожне натискання клавіші. Виконання важкої операції (запит до сервера, складне обчислення) на кожен такий виклик перевантажує браузер і сервер даремно.
Debounce відкладає виклик функції, доки не мине заданий час без нових викликів. Якщо функцію викликають знову до завершення затримки, попередній відкладений виклик скасовується й відлік починається заново — функція зрештою виконається лише один раз, після того як події перестали надходити:
function debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
const search = debounce((query) => {
console.log("Пошук:", query);
}, 300);
// Швидкі виклики поспіль — реальний пошук піде лише через 300мс після ОСТАННЬОГО
search("j");
search("ja");
search("java"); // лише цей виклик зрештою спрацюєThrottle гарантує, що функція виконується не частіше, ніж раз на заданий проміжок часу, незалежно від того, скільки разів її насправді викликали — на відміну від debounce, throttle не «чекає паузи», а рівномірно пропускає виклики:
function throttle(fn, interval) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= interval) {
lastCall = now;
fn(...args);
}
};
}
const onScroll = throttle(() => {
console.log("Позиція скролу:", window.scrollY);
}, 200);
window.addEventListener("scroll", onScroll); // спрацьовує не частіше, ніж раз на 200мсDebounce — виконує функцію один раз, після того як події ПРИПИНИЛИСЬ на заданий час. Приклад: пошук під час набору тексту, валідація поля після завершення вводу.
Throttle — виконує функцію РІВНОМІРНО, не частіше заданого інтервалу, поки події тривають. Приклад: обробник scroll, resize, оновлення позиції курсора при перетягуванні.
Практичне правило: якщо важлива лише фінальна дія користувача (він закінчив вводити текст) — debounce. Якщо важливо реагувати під час дії, але не на кожен піксель (постійно оновлювати індикатор прогресу скролу) — throttle.
У продакшн-коді debounce/throttle рідко пишуть із нуля — бібліотеки на кшталт lodash (_.debounce, _.throttle) чи хуки в React-екосистемі вже покривають крайні випадки (виклик із гарантією виконання останнього разу, скасування на розмонтуванні компонента). Розуміння механізму важливе для того, щоб свідомо обирати між ними, а не для щоденного написання з нуля.
Створювати новий debounced/throttled варіант функції всередині тіла React-компонента без useMemo/useRef — на кожному рендері з'являється нова функція з власним таймером, тож попередній відкладений виклик ніколи не скасовується так, як очікувалось.
Забути скасувати відкладений виклик при розмонтуванні компонента (clearTimeout у функції очищення useEffect) — debounced-функція може спрацювати вже після того, як компонент зник, і викликати оновлення стану неіснуючого компонента.
Плутати debounce і throttle на практиці — застосувати throttle там, де потрібен лише фінальний результат (наприклад, валідація поля під час набору тексту), означає зайві проміжні виклики; застосувати debounce там, де потрібна регулярна реакція під час дії (індикатор прогресу скролу), означає, що індикатор оновиться лише один раз — після того, як користувач зупинився.
Debounce відкладає виконання, доки події не припиняться на заданий час, і виконує функцію один раз — підходить, коли важлива лише фінальна дія. Throttle виконує функцію рівномірно, не частіше заданого інтервалу, поки події тривають, — підходить, коли потрібна регулярна реакція під час самої дії. В обох випадках у React критично прив'язати створення функції до useMemo/useRef і скасовувати відкладені виклики при розмонтуванні компонента.