Дебаунсинг событий

В интерфейсах на основе MapLibre GL JS события движения карты и взаимодействия с пользователем генерируются с высокой частотой. Панорамирование, масштабирование, вращение, движение мыши — всё это приводит к десяткам и сотням вызовов обработчиков в секунду. Без контроля частоты вызовов такие обработчики становятся узким местом: падает FPS, возрастает нагрузка на CPU, ухудшается отзывчивость интерфейса.

MapLibre GL JS генерирует события уровня рендера и взаимодействия:

  • move, moveend
  • zoom, zoomend
  • rotate, pitch
  • render
  • mousemove, mouseover, mouseout
  • data, idle

Некоторые из них, например render, могут вызываться десятки раз в секунду во время анимаций или перерисовки тайлов. Если внутри таких обработчиков выполняется логика:

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

то возникает перегрузка основного потока JavaScript.

Классическое решение — дебаунсинг (debouncing), ограничивающий частоту вызова функции.


Суть дебаунсинга

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

Иначе говоря:

  • события происходят часто
  • функция ждёт «тишины»
  • выполняется один раз после паузы

Формально:

  • если событие повторяется до истечения таймера — таймер сбрасывается
  • выполнение происходит только после последнего события

Базовая реализация debounce

Для MapLibre GL JS часто используют собственные реализации вместо сторонних библиотек:

function debounce(fn, delay) {
  let timer = null;

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

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

Эта функция создаёт «обёртку», которая:

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

Применение к событиям карты

Типичный сценарий — обработка move или zoom для обновления UI (например, координат центра карты или загрузки данных).

Без оптимизации

map.on('move', () => {
  const center = map.getCenter();
  updateSidebar(center);
});

Такой код будет вызываться десятки раз в секунду.


С debounce

const updateSidebarDebounced = debounce(() => {
  const center = map.getCenter();
  updateSidebar(center);
}, 200);

map.on('move', updateSidebarDebounced);

Теперь обновление происходит только после того, как пользователь перестал двигать карту на 200 мс.


Где debounce особенно эффективен

1. Загрузка данных по bbox

Частая ошибка — запросы к серверу на каждом move:

map.on('move', () => {
  const bounds = map.getBounds();
  fetchData(bounds);
});

С debounce:

const loadData = debounce(() => {
  const bounds = map.getBounds();
  fetchData(bounds);
}, 300);

map.on('move', loadData);

Это предотвращает лавину сетевых запросов при панорамировании.


2. Обновление интерфейса (панели, тултипы)

UI-элементы, зависящие от центра карты или масштаба:

  • координаты
  • уровень zoom
  • текущий регион

Оптимизация снижает количество reflow/repaint в DOM.


3. Поиск объектов в видимой области

При фильтрации GeoJSON-слоёв:

const filterFeatures = debounce(() => {
  const features = map.queryRenderedFeatures();
  updateList(features);
}, 150);

map.on('move', filterFeatures);

Ограничения классического debounce

Несмотря на эффективность, debounce имеет особенности:

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

В задачах, где важна непрерывная реакция (например, анимации), debounce не подходит.


Альтернатива: throttle

Для сравнения: throttle ограничивает вызовы до одного раза в заданный интервал.

Но в контексте MapLibre GL JS debounce чаще используется для:

  • завершённых действий (moveend, zoomend)
  • сетевых запросов
  • обновления тяжелых вычислений

Комбинация debounce и событий MapLibre

MapLibre GL JS предоставляет «сигнальные» события завершения взаимодействия:

  • moveend
  • zoomend

Их можно сочетать с debounce для дополнительной защиты:

const safeUpdate = debounce(() => {
  const bounds = map.getBounds();
  updateData(bounds);
}, 250);

map.on('moveend', safeUpdate);

Даже если moveend сработает несколько раз из-за инерции или программных вызовов, debounce предотвратит повторные вызовы.


Debounce с немедленным вызовом (leading edge)

Иногда требуется мгновенная реакция, а затем подавление повторов:

function debounceLeading(fn, delay) {
  let timer = null;
  let isFirstCall = true;

  return function (...args) {
    if (isFirstCall) {
      fn.apply(this, args);
      isFirstCall = false;
    }

    clearTimeout(timer);

    timer = setTimeout(() => {
      isFirstCall = true;
    }, delay);
  };
}

Применение:

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

Debounce через requestAnimationFrame

Для UI-операций, связанных с визуальными обновлениями карты, часто эффективнее синхронизация с рендер-циклом браузера:

function rafDebounce(fn) {
  let frame = null;

  return function (...args) {
    if (frame) cancelAnimationFrame(frame);

    frame = requestAnimationFrame(() => {
      fn.apply(this, args);
    });
  };
}

Использование:

map.on('move', rafDebounce(() => {
  const zoom = map.getZoom();
  updateZoomIndicator(zoom);
}));

Такой подход:

  • синхронизирует обновления с FPS
  • уменьшает layout thrashing
  • улучшает плавность интерфейса

Debounce в связке с состоянием приложения

В архитектуре приложений на MapLibre GL JS часто используется глобальное состояние (Redux, Zustand, MobX или собственные store).

Debounce применяется на уровне подписчиков:

store.subscribe(
  debounce(() => {
    const state = store.getState();
    syncMapFilters(state.filters);
  }, 200)
);

Это предотвращает каскад обновлений при массовых изменениях состояния.


Типичные ошибки при использовании debounce

1. Создание debounce внутри обработчика

map.on('move', () => {
  debounce(() => {
    console.log('bad');
  }, 200)();
});

Каждый вызов создаёт новую функцию и таймер, что полностью ломает механику.


2. Потеря контекста map

При использовании this:

map.on('move', debounce(function () {
  this.getCenter();
}, 200));

В MapLibre GL JS контекст может отличаться, поэтому предпочтительнее использовать стрелочные функции или явную привязку.


3. Чрезмерная задержка

Слишком большие значения (500–1000 мс) делают интерфейс «тяжёлым»:

  • задержка обновления UI
  • ощущение лагов
  • несинхронность с движением карты

Оптимальные диапазоны задержек

Практика использования в MapLibre GL JS:

  • 50–100 мс — UI-индикаторы, координаты
  • 150–300 мс — фильтрация данных, запросы API
  • 300–500 мс — тяжёлые вычисления, кластеризация

Связь debounce с производительностью рендера

MapLibre GL JS использует WebGL-рендеринг, но JavaScript-слой остаётся критическим:

  • обработка событий выполняется в main thread
  • чрезмерные вызовы блокируют render loop
  • debounce снижает нагрузку на event loop

Это особенно важно при:

  • мобильных устройствах
  • слабых CPU
  • сложных стилях с большим количеством слоёв

Архитектурный паттерн: event smoothing

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

  • debounce для завершённых действий
  • throttle для потоковых событий
  • requestAnimationFrame для UI-синхронизации
  • moveend/zoomend как сигналы стабильного состояния

Такой подход позволяет разделить:

  • интерактивную частоту событий
  • бизнес-логику обработки состояния
  • визуальное обновление интерфейса