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

9. debounce и throttle

⏱️ Управление временем: debounce, throttle и друзья

Заголовок раздела «⏱️ Управление временем: debounce, throttle и друзья»

Яша, время — деньги! ⏰ Сегодня учимся контролировать, как часто наш код реагирует на события. Это один из самых практичных уроков — ты будешь использовать эти операторы каждый день!


Представь: пользователь печатает в поисковой строке. Каждое нажатие клавиши — событие. Если на каждое нажатие отправлять запрос на сервер… 😱

  • Слово “javascript” = 10 запросов
  • 1000 пользователей = 10 000 запросов на одно слово

Нам нужно умно фильтровать события по времени!


debounceTime — это как кнопка закрытия двери лифта: нажал ещё раз — таймер сбрасывается. Лифт поедет только когда перестанешь нажимать (и пройдёт указанное время).

Мраморная диаграмма (debounceTime(300)):

События: --a-b-c--------d-e--|
300мс ↕ 300мс ↕
debounce: -----------c------e-|
import { fromEvent } from 'rxjs';
import { debounceTime, map } from 'rxjs/operators';
const searchInput = document.getElementById('search') as HTMLInputElement;
// Без debounce: запрос на КАЖДОЕ нажатие
fromEvent(searchInput, 'input').subscribe(() => {
// 🔴 "j" → запрос
// 🔴 "ja" → запрос
// 🔴 "jav" → запрос
// ... 10 запросов для слова "javascript"
});
// С debounceTime(300): запрос только после паузы 300мс
fromEvent(searchInput, 'input').pipe(
debounceTime(300),
map(e => (e.target as HTMLInputElement).value),
).subscribe(query => {
// ✅ Только один запрос после паузы в печатании!
fetch(`/api/search?q=${query}`);
});

💡 Правило большого пальца: debounceTime(300) для поиска, debounceTime(500) для менее срочных операций.


throttleTime — это кардиостимулятор: пульс не чаще одного раза в X миллисекунд. Первое событие проходит сразу, остальные в течение интервала — отбрасываются.

Мраморная диаграмма (throttleTime(500)):

События: --a-b-c-d-e----f-g-h-->
|--- 500мс ---|--- 500мс ---
throttle: --a-----------f--------->
import { fromEvent } from 'rxjs';
import { throttleTime, map } from 'rxjs/operators';
// Отслеживание позиции мыши
fromEvent(document, 'mousemove').pipe(
throttleTime(100), // не чаще 10 раз в секунду
map(e => ({ x: (e as MouseEvent).clientX, y: (e as MouseEvent).clientY }))
).subscribe(pos => {
updateCursorPosition(pos); // вместо 1000 вызовов/сек → 10
});
// Защита от спама кнопки
const btn = document.getElementById('action-btn')!;
fromEvent(btn, 'click').pipe(
throttleTime(1000) // не чаще 1 раза в секунду
).subscribe(() => {
doSomething();
});

Пользователь печатает: j-a-v-a-s-c-r-i-p-t (за 1 секунду)
↕ ↕
debounceTime(300) throttleTime(300)
Ждёт паузу Первое + периодически
Эмитит ПОСЛЕ Эмитит СРАЗУ
debounce: ---------------------------javascript
(ждёт 300мс после последнего нажатия)
throttle: --j-----------s-----------t
(первый символ, потом через каждые 300мс)
debounceTimethrottleTime
ЭмититПосле паузыСразу + периодически
Последнее значение✅ Всегда❌ Может потерять
Идеально дляПоиск, валидацияСкролл, мышь, аналитика

auditTime ждёт X миллисекунд, затем эмитит последнее значение за этот период. Похож на дебаунс, но таймер не сбрасывается.

Мраморная диаграмма (auditTime(500)):

События: --a-b-c---d-e------f-->
|-- 500мс --|-- 500мс --|
audit: ----------e-----------f-->
import { fromEvent } from 'rxjs';
import { auditTime, map } from 'rxjs/operators';
// Позиция скролла: отправляем аналитику не чаще раза в секунду,
// но берём ПОСЛЕДНЮЮ позицию
fromEvent(window, 'scroll').pipe(
auditTime(1000),
map(() => window.scrollY)
).subscribe(position => {
sendAnalytics({ scrollDepth: position });
});

sampleTime делает “снимок” последнего значения через каждые X миллисекунд, независимо от того, было ли новое событие.

import { interval, Subject } from 'rxjs';
import { sampleTime } from 'rxjs/operators';
// Мониторинг производительности: снимаем метрики каждую секунду
const fps$ = new Subject<number>();
fps$.pipe(
sampleTime(1000) // снимок каждую секунду
).subscribe(currentFps => {
console.log(`FPS: ${currentFps}`);
});

🏆 Реальный пример: форма с умной валидацией

Заголовок раздела «🏆 Реальный пример: форма с умной валидацией»
import { fromEvent, combineLatest } from 'rxjs';
import { debounceTime, map, distinctUntilChanged, startWith } from 'rxjs/operators';
const emailInput = document.getElementById('email') as HTMLInputElement;
const passwordInput = document.getElementById('password') as HTMLInputElement;
const email$ = fromEvent(emailInput, 'input').pipe(
debounceTime(500),
map(e => (e.target as HTMLInputElement).value),
distinctUntilChanged(),
startWith('')
);
const password$ = fromEvent(passwordInput, 'input').pipe(
debounceTime(300),
map(e => (e.target as HTMLInputElement).value),
startWith('')
);
// Валидируем форму только после паузы в вводе
combineLatest([email$, password$]).pipe(
map(([email, password]) => ({
emailValid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email),
passwordValid: password.length >= 8,
canSubmit: email.length > 0 && password.length > 0,
}))
).subscribe(({ emailValid, passwordValid, canSubmit }) => {
console.log({ emailValid, passwordValid, canSubmit });
// Обновляем UI только когда пользователь сделал паузу
});

Попробуйте примеры в интерактивном редакторе: