Skip to main content

Throttle у JavaScript

throttle, це техніка, яка обмежує частоту викликів функції: навіть якщо подія відбувається дуже часто, функція виконується не частіше ніж раз на заданий інтервал часу. Простими словами, правило звучить так: «викликай функцію не частіше ніж раз на N мілісекунд».

Теорія

TL;DR

  • Правило throttle: «не частіше ніж раз на N мс», незалежно від кількості подій.
  • Реалізація тримає час останнього виклику (lastCall) і пропускає новий, лише коли минув інтервал.
  • Перший виклик проходить одразу, наступні ігноруються до кінця вікна затримки.
  • Сценарії: scroll, mousemove, resize, нескінченний скрол, оновлення прогрес-бара.
  • Throttle, це «не частіше ніж»; debounce, це «не раніше ніж». Перший оновлює під час дії, другий, після неї.
  • Версія з Lodash додає опції leading і trailing, тобто виклик на початку та в кінці інтервалу.

Швидкий приклад

javascript
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

Припустімо, ви хочете стежити за тим, як користувач рухає мишею:

javascript
window.addEventListener('mousemove', () => { console.log('рух миші'); });

У чому проблема:

  • mousemove може спрацьовувати десятки разів на секунду (до 100-200 разів на секунду);
  • функція викликатиметься надто часто;
  • навантаження на процесор зростає, і продуктивність падає.

Якщо всередині обробника є читання геометрії (getBoundingClientRect) або зміна стилів, кожне зайве спрацювання ще й провокує додатковий layout, тож сторінка починає смикатися.

Як throttle працює всередині

javascript
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 спрацьовує сотні разів.

Покроково:

  1. На першому виклику функція виконується одразу.
  2. Зберігається час останнього виклику (lastCall).
  3. Усі наступні виклики ігноруються, доки не мине delay мілісекунд.
  4. Після закінчення тайм-вікна функцію знову можна викликати.

Зверніть увагу: ця найпростіша реалізація має лише leading-поведінку. Остання подія серії, яка потрапила всередину інтервалу, буде втрачена, бо trailing-виклику тут немає.

Приклади з реального коду

  1. Оновлення прогрес-бара при скролі:

    javascript
    window.addEventListener( 'scroll', throttle(() => { progressBar.value = window.scrollY; }, 100) );
  2. Відстеження позиції миші для анімації:

    javascript
    window.addEventListener( 'mousemove', throttle(updateCursorPosition, 50) );
  3. Перерахунок розмірів вікна (resize):

    javascript
    window.addEventListener( 'resize', throttle(updateLayout, 200) );

У багатьох проєктах беруть готовий варіант з Lodash:

javascript
import { throttle } from 'lodash'; const throttled = throttle(() => { console.log('scroll!'); }, 300); window.addEventListener('scroll', throttled);

Версія з Lodash просунутіша: вона вміє викликати функцію на початку і (або) в кінці інтервалу через опції leading і trailing, а також має метод cancel() для скасування відкладеного виклику.

Відмінність від debounce

ОсобливістьThrottleDebounce
МетаОбмежити частоту викликівВідкласти виконання, доки користувач не завершить дію
ВикликаєтьсяРегулярно, але не частіше ніж раз на N мсЛише один раз після паузи
Прикладscroll, mousemove, resizeinput, 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 не скасовує підписку, і обгортка далі смикає код уже неіснуючого екрана.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.