В интерфейсах на основе MapLibre GL JS события движения карты и взаимодействия с пользователем генерируются с высокой частотой. Панорамирование, масштабирование, вращение, движение мыши — всё это приводит к десяткам и сотням вызовов обработчиков в секунду. Без контроля частоты вызовов такие обработчики становятся узким местом: падает FPS, возрастает нагрузка на CPU, ухудшается отзывчивость интерфейса.
MapLibre GL JS генерирует события уровня рендера и взаимодействия:
move, moveendzoom, zoomendrotate, pitchrendermousemove, mouseover,
mouseoutdata, idleНекоторые из них, например render, могут вызываться
десятки раз в секунду во время анимаций или перерисовки тайлов. Если
внутри таких обработчиков выполняется логика:
то возникает перегрузка основного потока JavaScript.
Классическое решение — дебаунсинг (debouncing), ограничивающий частоту вызова функции.
Дебаунсинг — это техника, при которой функция выполняется только после того, как поток повторяющихся событий прекращается на заданный интервал времени.
Иначе говоря:
Формально:
Для 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);
});
Такой код будет вызываться десятки раз в секунду.
const updateSidebarDebounced = debounce(() => {
const center = map.getCenter();
updateSidebar(center);
}, 200);
map.on('move', updateSidebarDebounced);
Теперь обновление происходит только после того, как пользователь перестал двигать карту на 200 мс.
Частая ошибка — запросы к серверу на каждом 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);
Это предотвращает лавину сетевых запросов при панорамировании.
UI-элементы, зависящие от центра карты или масштаба:
Оптимизация снижает количество reflow/repaint в DOM.
При фильтрации GeoJSON-слоёв:
const filterFeatures = debounce(() => {
const features = map.queryRenderedFeatures();
updateList(features);
}, 150);
map.on('move', filterFeatures);
Несмотря на эффективность, debounce имеет особенности:
В задачах, где важна непрерывная реакция (например, анимации), debounce не подходит.
Для сравнения: throttle ограничивает вызовы до одного раза в заданный интервал.
Но в контексте MapLibre GL JS debounce чаще используется для:
MapLibre GL JS предоставляет «сигнальные» события завершения взаимодействия:
moveendzoomendИх можно сочетать с debounce для дополнительной защиты:
const safeUpdate = debounce(() => {
const bounds = map.getBounds();
updateData(bounds);
}, 250);
map.on('moveend', safeUpdate);
Даже если moveend сработает несколько раз из-за инерции
или программных вызовов, debounce предотвратит повторные вызовы.
Иногда требуется мгновенная реакция, а затем подавление повторов:
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);
};
}
Применение:
Для 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);
}));
Такой подход:
В архитектуре приложений на MapLibre GL JS часто используется глобальное состояние (Redux, Zustand, MobX или собственные store).
Debounce применяется на уровне подписчиков:
store.subscribe(
debounce(() => {
const state = store.getState();
syncMapFilters(state.filters);
}, 200)
);
Это предотвращает каскад обновлений при массовых изменениях состояния.
map.on('move', () => {
debounce(() => {
console.log('bad');
}, 200)();
});
Каждый вызов создаёт новую функцию и таймер, что полностью ломает механику.
При использовании this:
map.on('move', debounce(function () {
this.getCenter();
}, 200));
В MapLibre GL JS контекст может отличаться, поэтому предпочтительнее использовать стрелочные функции или явную привязку.
Слишком большие значения (500–1000 мс) делают интерфейс «тяжёлым»:
Практика использования в MapLibre GL JS:
MapLibre GL JS использует WebGL-рендеринг, но JavaScript-слой остаётся критическим:
Это особенно важно при:
В продвинутых приложениях debounce становится частью более широкой стратегии сглаживания событий:
Такой подход позволяет разделить: