Пошук уроків, статей та іншого контенту
Дві техніки обмеження частоти викликів функції — і коли застосовувати кожну.
У браузері деякі події можуть виникати дуже часто:
input — під час введення кожного символу;
scroll — під час прокручування сторінки;
mousemove — під час руху курсора;
resize — під час зміни розміру вікна.
Якщо на кожну таку подію запускати складну операцію, це може призвести до:
зайвих мережевих запитів;
високого навантаження на процесор;
ривків анімацій;
повільного інтерфейсу;
надмірних звернень до DOM.
debounce і throttle допомагають контролювати частоту викликів функції. Вони схожі за метою, але працюють по-різному.
Debounce відкладає виклик функції, доки подія не припиниться на вказаний проміжок часу.
Кожен новий виклик скидає таймер. Функція виконається лише після того, як виклики припиняться.
Умовно:
виклик → очікування
виклик → таймер починається знову
виклик → таймер починається знову
пауза → виконання функціїУявімо поле пошуку. Не варто відправляти запит на сервер після кожного натиснення клавіші. Користувач може ввести слово з десяти символів — і тоді браузер майже одразу створить десять запитів.
З debounce запит буде виконано після того, як користувач завершить введення:
function debounce(callback, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
callback.apply(this, args);
}, delay);
};
}
const searchInput = document.querySelector('#search');
const handleSearch = debounce((event) => {
const query = event.target.value.trim();
if (!query) {
return;
}
console.log('Пошук:', query);
}, 300);
searchInput.addEventListener('input', handleSearch);Якщо користувач вводить текст без пауз довше ніж 300 мс, таймер постійно перезапускається. Обробник спрацює лише після паузи в 300 мс.
Debounce добре підходить, коли важливий фінальний результат, а проміжні виклики не мають сенсу:
пошук під час введення;
валідація форми після завершення введення;
збереження чернетки після паузи;
фільтрація списку;
обчислення після зміни кількох параметрів;
обробка resize, коли потрібен лише кінцевий розмір вікна.
Throttle обмежує частоту викликів функції. Він дозволяє виконувати її не частіше одного разу за визначений проміжок часу.
Якщо подія виникає багато разів, функція все одно запускатиметься регулярно:
виклик → виконання
виклики → очікування
через інтервал → виконання
виклики → очікування
через інтервал → виконанняНа відміну від debounce, throttle не чекає повного завершення потоку подій.
Під час scroll браузер може генерувати багато подій. Якщо перевіряти позицію прокручування на кожній із них, це може бути зайвим.
function throttle(callback, interval) {
let lastCallTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastCallTime < interval) {
return;
}
lastCallTime = now;
callback.apply(this, args);
};
}
const handleScroll = throttle(() => {
const scrollPosition = window.scrollY;
console.log('Позиція прокручування:', scrollPosition);
}, 200);
window.addEventListener('scroll', handleScroll);У цьому прикладі обробник виконується не частіше ніж один раз на 200 мс, навіть якщо подія scroll виникає значно частіше.
Throttle підходить, коли важливо регулярно реагувати на процес:
відстеження прокручування;
оновлення координат під час руху курсора;
зміна розміру елементів;
перетягування елементів;
відображення прогресу;
синхронізація інтерфейсу зі станом прокручування;
обмеження кількості повторних дій користувача.
| Техніка | Принцип роботи | Типовий сценарій | |---|---|---| | debounce | Виклик після завершення серії подій | Пошук після введення | | throttle | Виклик через рівні проміжки під час серії подій | Обробка прокручування |
Просте правило:
якщо потрібен результат після паузи, використовуйте debounce;
якщо потрібно реагувати під час процесу, використовуйте throttle.
Іноді потрібно виконати функцію не після паузи, а одразу на початку серії викликів. Для цього можна додати параметр leading.
Також корисно мати можливість скасувати запланований виклик:
function debounce(callback, delay, options = {}) {
let timeoutId;
const { leading = false } = options;
function debounced(...args) {
const shouldCallImmediately = leading && !timeoutId;
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null;
if (!leading) {
callback.apply(this, args);
}
}, delay);
if (shouldCallImmediately) {
callback.apply(this, args);
}
}
debounced.cancel = function () {
clearTimeout(timeoutId);
timeoutId = null;
};
return debounced;
}
const handleSearch = debounce(
(event) => {
console.log('Пошук:', event.target.value);
},
300,
{ leading: false }
);У більшості випадків для пошуку потрібна поведінка trailing: функція запускається після завершення введення. Для кнопок або дій, які мають реагувати одразу, може бути корисним leading.
У простій реалізації throttle остання подія може бути втрачена. Наприклад, якщо користувач припинив прокручування одразу після запуску інтервалу, обробник не обов’язково отримає найновіше значення.
Розширена реалізація може виконувати функцію:
одразу на початку (leading);
один раз після завершення інтервалу (trailing).
function throttle(callback, interval) {
let timeoutId = null;
let lastArgs;
let lastThis;
function invoke() {
callback.apply(lastThis, lastArgs);
lastArgs = null;
lastThis = null;
}
function throttled(...args) {
lastArgs = args;
lastThis = this;
if (timeoutId) {
return;
}
invoke();
timeoutId = setTimeout(() => {
timeoutId = null;
if (lastArgs) {
invoke();
}
}, interval);
}
throttled.cancel = function () {
clearTimeout(timeoutId);
timeoutId = null;
lastArgs = null;
lastThis = null;
};
return throttled;
}Вибір між leading і trailing залежить від задачі. Для візуальних оновлень часто важливо реагувати одразу, але також обробити останнє значення після завершення дії.
requestAnimationFrame для візуальних оновленьThrottle із часовим інтервалом не завжди є найкращим рішенням для анімацій або оновлення стилів. Якщо функція змінює інтерфейс, варто розглянути requestAnimationFrame.
Браузер викличе передану функцію перед наступним перемальовуванням сторінки:
let animationFrameId = null;
function handleMouseMove(event) {
if (animationFrameId !== null) {
return;
}
animationFrameId = requestAnimationFrame(() => {
animationFrameId = null;
// Оновлення інтерфейсу синхронізоване з перемальовуванням браузера
console.log(event.clientX, event.clientY);
});
}
document.addEventListener('mousemove', handleMouseMove);Цей підхід особливо доречний, коли обробник:
змінює transform;
оновлює положення елемента;
змінює прогрес анімації;
візуально реагує на рух курсора або прокручування.
Фіксованого універсального значення не існує. Воно залежить від операції та вимог до інтерфейсу.
Орієнтовно:
100–200 мс — часті оновлення інтерфейсу;
200–400 мс — пошук і фільтрація;
500–1000 мс — автоматичне збереження або дорогі операції.
Занадто мала затримка не дасть суттєвого виграшу. Занадто велика зробить інтерфейс повільним або створить відчутну паузу перед результатом.
Значення потрібно перевіряти на реальному сценарії, а не вибирати механічно.
Неправильно створювати нову debounced-функцію під час кожної події:
input.addEventListener('input', (event) => {
debounce(() => {
console.log(event.target.value);
}, 300)();
});У такому випадку кожен виклик створює окремий таймер, тому скасування попереднього таймера не працює.
Правильно створити обмежену функцію один раз:
const handleInput = debounce((event) => {
console.log(event.target.value);
}, 300);
input.addEventListener('input', handleInput);thisЯкщо callback використовує this, важливо зберегти контекст. Саме для цього в реалізаціях застосовують:
callback.apply(this, args);Водночас стрілкові функції не мають власного this, тому контекст потрібно враховувати під час вибору типу функції.
Debounce для прокручування може призвести до того, що інтерфейс реагуватиме лише після завершення прокручування. Throttle для пошуку, навпаки, може створити кілька непотрібних запитів під час введення.
Вибір потрібно робити за поведінкою, яка потрібна користувачу:
чекати завершення — debounce;
оновлювати стан у процесі — throttle.
Якщо компонент видаляється, а таймер продовжує існувати, callback може виконатися після того, як його дані або DOM-елементи вже не актуальні.
У компонентах потрібно:
видаляти обробники подій;
викликати метод cancel, якщо реалізація його підтримує;
скасовувати пов’язані мережеві запити, якщо це необхідно.
Debounce і throttle лише зменшують кількість викликів. Вони не виправляють саму повільну операцію.
Якщо обробник виконує надто багато роботи, додатково варто:
зменшити обсяг обчислень;
не виконувати зайві операції з DOM;
використовувати делегування подій;
кешувати результати;
розглянути асинхронне виконання або вебпрацівників для важких обчислень.
Debounce і throttle обмежують частоту викликів функцій, але роблять це за різними правилами.
Debounce запускає функцію після того, як події припинилися на заданий час.
Throttle дозволяє запускати функцію не частіше за встановлений інтервал.
Для пошуку, фільтрації та автозбереження зазвичай використовують debounce.
Для прокручування, перетягування та регулярних оновлень — throttle.
Для синхронізації візуальних змін із перемальовуванням браузера часто підходить requestAnimationFrame.
Обмеження частоти потрібно створювати один раз, а не під час кожної події.