Skip to content

Декоратор withThrottle

Декоратор withThrottle (заслонка) — это инструмент оптимизации, который применяется в сценариях с высокой частотой генерации событий, когда нам важен непрерывный процесс изменений в динамике, но с жестким ограничением максимальной частоты вызовов.

В отличие от дебаунса, который бесконечно откладывает выполнение запроса и ждет наступления полной «тишины», троттлинг гарантированно пропускает самый первый вызов мгновенно (Leading edge), а затем равномерно порциями (например, строго раз в 300 мс) досылает актуальные данные. Это позволяет приложению реагировать на действия пользователя прямо в процессе их совершения.

Практические кейсы применения декоратора withThrottle

ts
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.

Выбор правильного инструмента зависит от того, какую природу имеет операция — синхронную или асинхронную.

📊 Краткая матрица выбора

КритерийwithThrottlewithThrottleComputed
Где применятьВнутри асинхронных ресурсов (engine.resource)Внутри синхронных вычислений (engine.computed)
Характер операцииАсинхронные запросы (API, операции с БД, тяжелые воркеры)Синхронная фильтрация, маппинг примитивов, UI-координаты
Контракт функцииasync (source, abortSignal) => Promise<T>() => T (Чистая синхронная лямбда)
Поведение UIПереключает статус loading: true / falseМгновенно отдает зафиксированное в кэше значение

Подробный разбор

1. withThrottle (Для асинхронных Ресурсов)

Применяется строго для ограничения частоты тяжелых асинхронных операций, которые зависят от реактивного состояния.

  • Когда использовать: Вы хотите отправлять поисковый запрос на бэкенд при вводе текста, но не чаще раза в 300 мс. Или хотите отправлять гео-аналитику координат мыши на сервер.
  • Как это работает: Декоратор оборачивает асинхронную функцию внутри engine.resource. Граф реактивности ядра остается синхронным, но сам вызов сетевого метода зажимается в тиски лимита времени.
typescript
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-компоненты.
  • Как это работает: Он перехватывает чтение сырого сигнала, подписывается на него, но обновляет результирующий стейт строго по таймеру, отсекая промежуточный дребезг.
typescript
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 внутри обычного ресурса, оставив входной сигнал координат сырым.

Что происходит под капотом (Проблема):

  1. Курсор движется: сигнал coordsSignal меняется на каждый пиксель (100 раз за секунду).
  2. Синхронный engine.resource мгновенно видит, что его зависимость изменилась.
  3. Он каждый раз переводит свой внутренний стейт в loading: true и пытается запустить функцию.
  4. Декоратор withThrottle внутри ресурса честно блокирует сам асинхронный расчет, но флаг загрузки в ресурсе уже взведен!
  5. На следующий пиксель всё повторяется. В итоге внутренний сигнал ресурса начинает спамить микрозадачными перерисовками loading: true / false прямо в интерфейс React/Vue. В профайлере ядра загорается предупреждение ⚠️ (high noise - сигнал менялся слишком часто), а интерфейс начинает мерцать и лагать.

Идеальное архитектурное решение (Плюсы использования withThrottleComputed на входе):

Если пропустить сырые координаты мыши через withThrottleComputed, поток данных разделяется на три чистых этапа:

[Сырой Сигнал Мыши] (Спам 400 тиков)

[withThrottleComputed] (Сжатие потока)

(Тикает строго 1 раз в 300 мс)
[Асинхронный Ресурс] (loading: true включается РOВНО 1 раз в 300 мс)

Главные преимущества такой схемы:

  1. Zero UI Noise (Никакого мерцания): Индикатор загрузки ⏳ Расчёт... плавно загорается и гаснет ровно один раз за такт, интерфейс не дергается и работает плавно.
  2. Максимальная производительность: Количество ресурсоемких проверок и ререндеров React/Vue компонентов уменьшается в десятки раз.
  3. Чистый лог транзакций: Подсистема логгирования ядра вместо предупреждений о шуме выводит аккуратную, строго дозированную и красивую историю транзакций.