Throttle у JavaScript
throttle, це техніка, яка обмежує частоту викликів функції: навіть якщо подія відбувається дуже часто, функція виконується не частіше ніж раз на заданий інтервал часу. Простими словами, правило звучить так: «викликай функцію не частіше ніж раз на N мілісекунд».
Теорія
TL;DR
- Правило throttle: «не частіше ніж раз на N мс», незалежно від кількості подій.
- Реалізація тримає час останнього виклику (
lastCall) і пропускає новий, лише коли минув інтервал. - Перший виклик проходить одразу, наступні ігноруються до кінця вікна затримки.
- Сценарії:
scroll,mousemove,resize, нескінченний скрол, оновлення прогрес-бара. - Throttle, це «не частіше ніж»; debounce, це «не раніше ніж». Перший оновлює під час дії, другий, після неї.
- Версія з Lodash додає опції
leadingіtrailing, тобто виклик на початку та в кінці інтервалу.
Швидкий приклад
function throttle(fn, delay) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
fn.apply(this, args);
}
};
}
const handleMove = throttle(() => {
console.log('Рух миші оброблено');
}, 500);
window.addEventListener('mousemove', handleMove);Проблема: обробник без throttle
Припустімо, ви хочете стежити за тим, як користувач рухає мишею:
window.addEventListener('mousemove', () => {
console.log('рух миші');
});У чому проблема:
mousemoveможе спрацьовувати десятки разів на секунду (до 100-200 разів на секунду);- функція викликатиметься надто часто;
- навантаження на процесор зростає, і продуктивність падає.
Якщо всередині обробника є читання геометрії (getBoundingClientRect) або зміна стилів, кожне зайве спрацювання ще й провокує додатковий layout, тож сторінка починає смикатися.
Як throttle працює всередині
function throttle(fn, delay) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
fn.apply(this, args);
}
};
}
const handleMove = throttle(() => {
console.log('Рух миші оброблено');
}, 500);
window.addEventListener('mousemove', handleMove);Тепер функція handleMove викликається не частіше ніж 1 раз на 500 мс, навіть якщо mousemove спрацьовує сотні разів.
Покроково:
- На першому виклику функція виконується одразу.
- Зберігається час останнього виклику (
lastCall). - Усі наступні виклики ігноруються, доки не мине
delayмілісекунд. - Після закінчення тайм-вікна функцію знову можна викликати.
Зверніть увагу: ця найпростіша реалізація має лише leading-поведінку. Остання подія серії, яка потрапила всередину інтервалу, буде втрачена, бо trailing-виклику тут немає.
Приклади з реального коду
-
Оновлення прогрес-бара при скролі:
javascriptwindow.addEventListener( 'scroll', throttle(() => { progressBar.value = window.scrollY; }, 100) ); -
Відстеження позиції миші для анімації:
javascriptwindow.addEventListener( 'mousemove', throttle(updateCursorPosition, 50) ); -
Перерахунок розмірів вікна (
resize):javascriptwindow.addEventListener( 'resize', throttle(updateLayout, 200) );
У багатьох проєктах беруть готовий варіант з Lodash:
import { throttle } from 'lodash';
const throttled = throttle(() => {
console.log('scroll!');
}, 300);
window.addEventListener('scroll', throttled);Версія з Lodash просунутіша: вона вміє викликати функцію на початку і (або) в кінці інтервалу через опції leading і trailing, а також має метод cancel() для скасування відкладеного виклику.
Відмінність від debounce
| Особливість | Throttle | Debounce |
|---|---|---|
| Мета | Обмежити частоту викликів | Відкласти виконання, доки користувач не завершить дію |
| Викликається | Регулярно, але не частіше ніж раз на N мс | Лише один раз після паузи |
| Приклад | scroll, mousemove, resize | input, search, autocomplete |
| Поведінка | «Не частіше ніж» | «Не раніше ніж» |
Наочно:
- Throttle: під час скролу сторінки оновлюємо позицію кожні 100 мс.
- Debounce: чекаємо, доки користувач закінчить друкувати, і лише потім робимо запит.
Підсумок:
| Пункт | Опис |
|---|---|
| Що робить | Обмежує частоту виклику функції |
| Коли використовувати | При частих подіях: scroll, resize, mousemove |
| Аргументи | Функція і затримка (мс) |
| Повертає | Нову обгорнуту функцію |
| Користь | Знижує навантаження на CPU, підвищує продуктивність |
| Відмінність від debounce | Виконується регулярно, але не частіше ніж заданий інтервал |
Типові помилки
- Створювати обгортку на кожне спрацювання:
addEventListener('scroll', throttle(fn, 100))усередині іншого обробника чи рендера щоразу дає нову функцію з нульовимlastCall, і обмеження не працює. - Забувати про втрачений
trailing-виклик: у наївній реалізації остання подія серії не обробляється, тому фінальна позиція скролу може лишитися незафіксованою. Для цього беруть Lodash або додаютьsetTimeoutна хвіст інтервалу. - Плутати throttle з debounce: для живого пошуку throttle надішле запит кожні N мс просто під час набору тексту, а потрібен один запит після паузи.
- Використовувати throttle для анімації замість
requestAnimationFrame: для візуальних оновлень rAF синхронізований з кадрами браузера і дає плавніший результат. - Брати
Date.now()там, де потрібна монотонність: системний годинник може перевестися, тому для точних інтервалів надійнішеperformance.now(). - Не знімати обробник при розмонтуванні компонента: throttle не скасовує підписку, і обгортка далі смикає код уже неіснуючого екрана.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.