Декоратор withThrottle
Декоратор withThrottle (заслонка) — это инструмент оптимизации, который применяется в сценариях с высокой частотой генерации событий, когда нам важен непрерывный процесс изменений в динамике, но с жестким ограничением максимальной частоты вызовов.
В отличие от дебаунса, который бесконечно откладывает выполнение запроса и ждет наступления полной «тишины», троттлинг гарантированно пропускает самый первый вызов мгновенно (Leading edge), а затем равномерно порциями (например, строго раз в 300 мс) досылает актуальные данные. Это позволяет приложению реагировать на действия пользователя прямо в процессе их совершения.
Практические кейсы применения декоратора withThrottle
const throttledMapResource = engine.resource(
withThrottle(
async (bboxValue, abortSignal) => {
const res = await fetch(`/api/stations?bbox=${bboxValue}`, { signal: abortSignal });
return res.json();
},
// Запросы к API будут улетать не чаще, чем 1 раз в 400 миллисекунд
{ limit: 400 }
),
bboxSignal
);1. Плавный ресайз тяжелых интерфейсов (Window Resize / WebGL & Canvas)
- Проблема без декоратора: При изменении размеров окна браузера (когда пользователь плавно тянет за угол экрана) событие
resizeгенерируется непрерывно десятки раз в секунду. Если на каждый пиксель запускать тяжелый пересчет матриц проекции 3D-сцены (Three.js), перезапуск физики (Phaser/Kaplay) или перерисовку сложных SVG-графиков дашборда, процессор мгновенно загрузится на 100%, и интерфейс начнет намертво зависать и дергаться. - Решение с Throttle: Вы выставляете лимит
limit: 50(20 кадров в секунду для ресайза). При перетаскивании окна холст графики плавно и адаптивно подстраивается под новые размеры порциями, не перегружая CPU. Хвостовой вызов (Trailing edge) гарантирует, что когда пользователь отпустит мышь, сцена идеально встанет под финальный размер экрана.
2. Бесконечная лента и пагинация при прокрутке (Scroll Events / Infinite Scroll)
- Проблема: Нам нужно реализовать бесконечную подгрузку постов, когда пользователь доскроллил до конца страницы. Если повесить проверку координат скролла (
window.scrollY) на стандартное событиеscroll, браузер будет выполнять математические вычисления на каждый сдвиг колеса мыши. Это приводит к так называемому «jank-эффекту» — микрофризам и дерганью прокрутки, особенно на мобильных устройствах. - Решение с Throttle: Выставляется лимит
limit: 100–200мс. Браузер вычисляет расстояние до низа страницы всего несколько раз в секунду. Этого более чем достаточно, чтобы вовремя и бесшовно инициировать подгрузку следующей порции контента, сохраняя при этом идеальную нативную плавность прокрутки (60+ FPS).
3. Отслеживание перемещения курсора или тач-событий (Mouse Move / Drag & Drop)
- Проблема: В приложении разрабатывается интерактивный инструмент (например, холст для рисования, Drag & Drop аналитических карточек на дашборде или кастомный графический курсор, подгружающий координаты). Нативные события
mousemoveиtouchmoveспамят координатами слишком часто. Если отправлять эти данные в реактивное состояние на каждый миг движения — граф зависимостей стейт-менеджера будет перегружен ежеминутными циклами обновлений. - Решение с Throttle: Троттлинг ограничивает поток координат до комфортных, например, 30 вызовов в секунду (
limit: 33). Физическая траектория движения и отзывчивость интерфейса полностью сохраняются, но нагрузка на реактивный движок и рендеринг UI снижается в разы.
4. Глобальный Rate Limiting для кнопок отправки данных (UI Spam Protection)
- Проблема: Пользователь из-за плохого интернет-соединения или нетерпения начинает яростно кликать по кнопке «Обновить данные», «Повторить запрос» или «Поставить лайк». Если на каждый клик слать сетевой запрос, клиент создаст искусственную DDOS-нагрузку на сервер API, отправляя кучу дублирующих транзакций.
- Решение с Throttle: Кнопка оборачивается в троттлинг с лимитом
limit: 1000(1 секунда). Первый клик улетает на сервер мгновенно (Leading edge), давая моментальный отклик. Все последующие яростные клики в течение секунды полностью блокируются, защищая бэкенд и сохраняя стабильность бизнес-логики.
Сводная шпаргалка по декораторам ресурсов:
withThrottle— Нужен, когда важен процесс в динамике, но порциями (пример: плавное изменение размеров куба Three.js, отслеживание скролла ленты, защита кнопок от спама).withCache— Нужен, когда данные редко меняются, и мы хотим полностью исключить повторные запросы при возвращении к прежним параметрам (пример: переключение табов, пагинация назад).withDebounce— Нужен, когда важен только финальный результат после того, как пользователь полностью затих (пример: валидация формы при вводе email, живой поиск).withThrottleAndCache— Нужен, когда важен процесс в динамике, но данные внутри этого процесса имеют свойство повторяться на коротком промежутке времени (пример: перемещение интерактивных карт, умный живой поиск).
⏱️ Оптимизация производительности: Троттлинг в ReactiveEngine
Для предотвращения высокой частоты обновлений и защиты интерфейса от лагов при интенсивном спаме входных данных (например, onMouseMove, onScroll, onInput), библиотека поставляет два специализированных декоратора троттлинга: withThrottle и withThrottleComputed.
Выбор правильного инструмента зависит от того, какую природу имеет операция — синхронную или асинхронную.
📊 Краткая матрица выбора
| Критерий | withThrottle | withThrottleComputed |
|---|---|---|
| Где применять | Внутри асинхронных ресурсов (engine.resource) | Внутри синхронных вычислений (engine.computed) |
| Характер операции | Асинхронные запросы (API, операции с БД, тяжелые воркеры) | Синхронная фильтрация, маппинг примитивов, UI-координаты |
| Контракт функции | async (source, abortSignal) => Promise<T> | () => T (Чистая синхронная лямбда) |
| Поведение UI | Переключает статус loading: true / false | Мгновенно отдает зафиксированное в кэше значение |
Подробный разбор
1. withThrottle (Для асинхронных Ресурсов)
Применяется строго для ограничения частоты тяжелых асинхронных операций, которые зависят от реактивного состояния.
- Когда использовать: Вы хотите отправлять поисковый запрос на бэкенд при вводе текста, но не чаще раза в 300 мс. Или хотите отправлять гео-аналитику координат мыши на сервер.
- Как это работает: Декоратор оборачивает асинхронную функцию внутри
engine.resource. Граф реактивности ядра остается синхронным, но сам вызов сетевого метода зажимается в тиски лимита времени.
import { AbstractService, withThrottle } from '@pravosleva/reactive-engine'
export class Throttle2DLogic extends AbstractService {
// ...
// Пример: Троттлинг отправки поискового запроса на API
public searchResource = this.engine.resource(
withThrottle(
async (query, abortSignal) => {
return await api.search(query, { signal: abortSignal })
},
{ limit: 400 }
),
this.searchQuerySignal
)
}2. withThrottleComputed (Для синхронных Вычислений)
Применяется на входе в реактивный граф для изоляции сырого спама данных.
- Когда использовать: Когда входной сигнал генерирует сотни событий в секунду (координаты мыши, пиксели скролла), и вам нужно «сжать» этот поток до дозированных тиков (например, раз в 300 мс) перед тем, как пустить его дальше в другие сервисы, computed-свойства или тяжелые UI-компоненты.
- Как это работает: Он перехватывает чтение сырого сигнала, подписывается на него, но обновляет результирующий стейт строго по таймеру, отсекая промежуточный дребезг.
import { AbstractService, withThrottleComputed } from '@pravosleva/reactive-engine'
export class Throttle2DLogic extends AbstractService {
// ...
// Пример: Троттлинг мышиного трекинга перед расчетом тяжелой гео-модели
public rawCoords = this.engine.signal({ x: 0, y: 0 }, 'signal:raw-coords')
// Зажимаем спам координат на этапе вычислений
public throttledCoords = withThrottleComputed(
this.engine,
() => this.rawCoords.value,
{ limit: 300 },
'computed:throttled-coords'
)
}⚠️ Архитектурная уязвимость: Почему нельзя троттлить асинхронную функцию внутри синхронного ресурса при спаме координат?
Частая ошибка — обернуть асинхронную функцию мышиного трекинга в withThrottle внутри обычного ресурса, оставив входной сигнал координат сырым.
Что происходит под капотом (Проблема):
- Курсор движется: сигнал
coordsSignalменяется на каждый пиксель (100 раз за секунду). - Синхронный
engine.resourceмгновенно видит, что его зависимость изменилась. - Он каждый раз переводит свой внутренний стейт в
loading: trueи пытается запустить функцию. - Декоратор
withThrottleвнутри ресурса честно блокирует сам асинхронный расчет, но флаг загрузки в ресурсе уже взведен! - На следующий пиксель всё повторяется. В итоге внутренний сигнал ресурса начинает спамить микрозадачными перерисовками
loading: true / falseпрямо в интерфейс React/Vue. В профайлере ядра загорается предупреждение⚠️ (high noise - сигнал менялся слишком часто), а интерфейс начинает мерцать и лагать.
Идеальное архитектурное решение (Плюсы использования withThrottleComputed на входе):
Если пропустить сырые координаты мыши через withThrottleComputed, поток данных разделяется на три чистых этапа:
[Сырой Сигнал Мыши] (Спам 400 тиков)
↓
[withThrottleComputed] (Сжатие потока)
↓
(Тикает строго 1 раз в 300 мс)
[Асинхронный Ресурс] (loading: true включается РOВНО 1 раз в 300 мс)Главные преимущества такой схемы:
- Zero UI Noise (Никакого мерцания): Индикатор загрузки
⏳ Расчёт...плавно загорается и гаснет ровно один раз за такт, интерфейс не дергается и работает плавно. - Максимальная производительность: Количество ресурсоемких проверок и ререндеров React/Vue компонентов уменьшается в десятки раз.
- Чистый лог транзакций: Подсистема логгирования ядра вместо предупреждений о шуме выводит аккуратную, строго дозированную и красивую историю транзакций.