Debouncing и throttling

При работе с WebGL-визуализациями на основе Deck.gl ключевым фактором производительности становится контроль частоты обновлений состояния и пересчёта слоёв. Даже при аппаратном ускорении рендеринг сцены остаётся дорогой операцией, особенно при больших объёмах геоданных, сложных шейдерах и интерактивных событиях (pan, zoom, hover, drag). В этих условиях механизмы debouncing и throttling выступают базовыми инструментами стабилизации потока событий и снижения нагрузки на рендер-пайплайн.


Причины появления избыточных обновлений

В типичной карте или аналитической сцене Deck.gl обновления приходят из нескольких источников:

  • события перемещения карты (viewport change)
  • движение мыши (pointermove)
  • изменение фильтров и параметров визуализации
  • потоковые данные (WebSocket, polling)
  • события интерфейса (ползунки, чекбоксы, ввод текста)

Большая часть этих событий генерируется с высокой частотой, часто десятки или сотни раз в секунду. Без контроля это приводит к:

  • избыточным вызовам setProps
  • частым пересчётам layers
  • повторному созданию WebGL буферов
  • деградации FPS

Deck.gl спроектирован так, чтобы минимизировать лишние рендеры, но логика приложения часто требует дополнительного контроля.


Throttling как механизм ограничения частоты

Throttling ограничивает частоту выполнения функции фиксированным интервалом времени. Независимо от количества входящих событий, функция вызывается не чаще одного раза за заданный промежуток.

Базовая идея

Функция выполняется:

  • сразу
  • затем блокируется на определённое время
  • последующие вызовы игнорируются до окончания интервала

Применение в Deck.gl

Throttling особенно полезен в сценариях:

  • обновление камеры (viewState)
  • обработка движения курсора над слоями
  • синхронизация с внешними источниками данных
  • реакция на scroll в больших списках данных, влияющих на карту

Пример реализации

function throttle(fn, interval) {
  let lastTime = 0;

  return function (...args) {
    const now = Date.now();

    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

Использование с Deck.gl

const updateViewState = throttle((viewState) => {
  deckgl.setProps({ viewState });
}, 50);

В этом случае обновление карты происходит максимум 20 раз в секунду, даже если события приходят чаще.


Debouncing как стратегия стабилизации финального состояния

Debouncing откладывает выполнение функции до момента, когда поток событий прекращается на заданное время. В отличие от throttling, промежуточные состояния игнорируются, фиксируется только итоговое.

Поведение

  • событие происходит → таймер запускается
  • новое событие сбрасывает таймер
  • выполнение происходит только после паузы

Типичные сценарии в Deck.gl

Debouncing применяется там, где промежуточные состояния не имеют смысла:

  • ввод текста в фильтрах (поиск по геоданным)
  • изменение диапазона параметров (slider)
  • загрузка данных по bbox после завершения перемещения карты
  • пересчёт сложных агрегатов

Реализация

function debounce(fn, delay) {
  let timeoutId;

  return function (...args) {
    clearTimeout(timeoutId);

    timeoutId = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

Пример использования

const fetchData = debounce(async (bounds) => {
  const data = await loadGeoData(bounds);
  deckgl.setProps({ layers: createLayers(data) });
}, 300);

Загрузка данных происходит только после завершения серии перемещений карты.


Отличие моделей поведения

Throttling

  • выполняет функцию регулярно
  • сохраняет промежуточные состояния
  • полезен для анимаций и интерактивного движения
  • предсказуемая частота вызова

Debouncing

  • выполняет функцию один раз после серии событий
  • игнорирует промежуточные состояния
  • подходит для вычислительно тяжёлых операций
  • ориентирован на финальный результат

Влияние на рендеринг Deck.gl

Deck.gl использует реактивную модель обновления через изменение props. Каждый вызов:

deckgl.setProps({ viewState, layers })

может привести к:

  • пересозданию WebGL команд
  • пересчёту атрибутов геометрии
  • перерисовке всех слоёв

Без контроля частоты обновлений возникают узкие места:

1. Перегрузка GPU

Слишком частые изменения viewState приводят к постоянному пересчёту матриц трансформации.

2. Перегрузка CPU

Создание новых массивов данных и слоёв при каждом событии увеличивает нагрузку на JavaScript-движок.

3. Блокировка main thread

Сложные слои (HexagonLayer, GridLayer, PathLayer) могут занимать значительное время на подготовку данных.


Комбинированное использование debounce и throttle

В реальных приложениях оба подхода часто комбинируются в одном потоке событий.

Пример архитектуры обработки камеры

const throttledRender = throttle((viewState) => {
  deckgl.setProps({ viewState });
}, 16);

const debouncedDataLoad = debounce((viewState) => {
  loadData(viewState);
}, 250);

function onViewStateChange({ viewState }) {
  throttledRender(viewState);
  debouncedDataLoad(viewState);
}

Поведение схемы

  • throttling обеспечивает плавное движение карты
  • debouncing инициирует загрузку данных только после остановки взаимодействия

Ошибки при использовании

1. Слишком агрессивный throttling

Интервалы больше 100–150 мс приводят к:

  • рывкам интерфейса
  • визуальной дискретности движения
  • рассинхронизации pointer events

2. Слишком короткий debounce

Значения менее 100–150 мс:

  • приводят к частым перезапросам данных
  • фактически превращают debouncing в слабый throttle

3. Смешивание без разделения ответственности

Использование одной обёртки для всех типов событий:

  • ухудшает читаемость
  • усложняет контроль производительности
  • создаёт непредсказуемые задержки

Использование с React и Deck.gl

В связке с React подход требует дополнительной осторожности из-за повторного рендера компонентов.

Пример с useCallback

const handleViewStateChange = useCallback(
  throttle(({ viewState }) => {
    setViewState(viewState);
  }, 16),
  []
);

При этом важно учитывать, что создание throttle внутри render без мемоизации приводит к потере состояния lastTime.

Debounce с useEffect

useEffect(() => {
  const handler = debounce(() => {
    fetchData(viewState);
  }, 300);

  handler();

  return () => {
    // очистка таймера происходит внутри debounce
  };
}, [viewState]);

Связь с архитектурой слоёв Deck.gl

Layer-парадигма Deck.gl подразумевает, что каждый слой реагирует на изменения props. Поэтому оптимизация через debouncing/throttling влияет не только на UI, но и на:

  • diff алгоритм слоёв
  • перерасчёт instance attributes
  • batching WebGL команд

Особенно критично это для:

  • ScatterplotLayer при большом количестве точек
  • GeoJsonLayer при сложной геометрии
  • TripsLayer при анимации

Оптимизация потоков событий

В высоконагруженных сценах применяется многоуровневая схема контроля:

  • raw events (mousemove, move)
  • throttled viewport updates
  • debounced data fetching
  • мемоизация слоёв
  • кэширование геометрии

Такая схема снижает нагрузку на:

  • CPU (обработка событий)
  • GPU (рендер)
  • память (пересоздание объектов)

Практика выбора интервалов

Типовые значения зависят от сценария:

  • 16–33 мс: интерактивное движение камеры (≈60 FPS)
  • 50–100 мс: мягкое ограничение событий pointermove
  • 200–300 мс: загрузка данных после остановки взаимодействия
  • 300–500 мс: текстовый ввод и фильтры

Эти значения не фиксированы, но отражают баланс между отзывчивостью и стоимостью вычислений.


Поток событий в типичном приложении Deck.gl

PointerMove → throttle → update viewState → render
                     ↓
                 debounce → fetch data → update layers

Такая модель разделяет:

  • визуальную реакцию
  • вычислительную нагрузку
  • сетевые операции

что критично для устойчивой работы сложных визуализаций.