В MapLibre GL JS любая интерактивность — движение карты, обновление источников данных, изменение стиля — приводит к перерасчёту сцены и потенциальному перерисовыванию WebGL-контекста. В отличие от классического DOM, здесь даже небольшие изменения могут запускать полный цикл рендеринга.
Throttling (ограничение частоты обновлений) в этом контексте означает контроль над тем, как часто инициируются операции, вызывающие:
Без контроля эти операции легко перегружают основной поток и WebGL-пайплайн.
MapLibre GL JS использует событийно-ориентированный рендеринг с
привязкой к requestAnimationFrame.
Ключевая особенность: карта не перерисовывается постоянно, но переходит в режим активного рендеринга при изменениях состояния.
Срабатывание рендера происходит при:
setPaintProperty,
setLayoutProperty)source.setData)map.triggerRepaint()После завершения всех изменений карта стремится к состоянию
idle, когда нет pending-операций.
Проблема не в одном вызове, а в их плотности.
При высокочастотных событиях:
mousemoveкарта может начать:
Особенно дорого стоят:
source.setData() для больших GeoJSONexpressions)Событие move может вызываться десятки раз в секунду во
время жеста.
Типичная ошибка — обновление UI или источников напрямую:
map.on('move', () => {
updateMarkers();
});
Это приводит к избыточной нагрузке.
Самый частый источник перегрузки при работе с интерактивными слоями:
map.on('mousemove', (e) => {
highlightFeature(e.point);
});
Без throttling это превращается в сотни вызовов в секунду.
Событие render вызывается каждый кадр во время активного
рендеринга. Любая логика внутри него должна быть минимальной.
Самый естественный для MapLibre способ синхронизации с рендером.
Идея: объединять все обновления в один кадр.
let pending = false;
function updateState() {
if (pending) return;
pending = true;
requestAnimationFrame(() => {
pending = false;
syncUIWithMap();
});
}
Преимущество: идеально синхронизируется с WebGL-циклом.
Подходит для событий вне render loop.
import throttle from "lodash.throttle";
const updateOnMove = throttle(() => {
refreshData();
}, 100);
map.on('move', updateOnMove);
Используется для:
Для событий, где важно только завершение действия:
import debounce from "lodash.debounce";
const onMove End = debounce(() => {
loadVisibleFeatures();
}, 200);
map.on('move', onMoveEnd);
Подходит для:
Одна из самых дорогих операций:
map.getSource('points').setData(geojson);
При потоковых данных важно агрегировать изменения:
let buffer = [];
function pushPoint(point) {
buffer.push(point);
}
setInterval(() => {
if (buffer.length === 0) return;
const source = map.getSource('points');
source.setData({
type: "FeatureCollection",
features: buffer
});
buffer = [];
}, 500);
Часто проблема не в частоте, а в объёме:
Vector Tiles вместо GeoJSONИзменения камеры — setCenter, setZoom,
flyTo — сами по себе оптимизированы, но их вызовы могут
быть избыточными.
Пример плохой практики:
map.on('mousemove', (e) => {
map.setCenter([e.lngLat.lng, e.lngLat.lat]);
});
Это приводит к рекурсивному рендер-шторму.
Корректный вариант:
const moveCamera = throttle((lngLat) => {
map.setCenter(lngLat);
}, 50);
Одна из ключевых оптимизаций — отделение частого UI-обновления от тяжёлого MapLibre-рендера.
map.on('move', () => {
updateSidebar(); // лёгкая операция
setSourceData(); // тяжёлая операция
});
map.on('move', updateSidebar);
const updateDataThrottled = throttle(() => {
setSourceData();
}, 200);
map.on('move', updateDataThrottled);
При работе с WebSocket или MQTT потоками важна агрегация:
const queue = [];
socket.onmess age = (msg) => {
queue.push(JSON.parse(msg.data));
};
setInterval(() => {
if (!queue.length) return;
const source = map.getSource('stream');
source.setData({
type: "FeatureCollection",
features: queue.splice(0)
});
}, 1000);
Дополнительная оптимизация:
Custom layers в MapLibre используют render callback:
render(gl, matrix) {
draw(gl);
map.triggerRepaint();
}
Без throttling triggerRepaint() создаёт бесконечный цикл
рендера.
Контроль:
let needsUpdate = true;
render(gl, matrix) {
if (!needsUpdate) return;
draw(gl);
needsUpdate = false;
}
map.on('move', throttle(() => {
update();
}, 100));
Это создаёт новый throttled-объект при каждом событии.
Правильно:
const throttledUpdate = throttle(update, 100);
map.on('move', throttledUpdate);
MapLibre предоставляет состояние покоя карты:
map.on('idle', () => {
finalizeHeavyOperations();
});
Использование idle позволяет полностью убрать ручной
throttling в некоторых сценариях.
Даже throttled вызовы setData могут быть слишком частыми
при больших GeoJSON.
Решение — переход на:
На практике используется сочетание подходов:
requestAnimationFrame для UIthrottle для сетевых запросовdebounce для завершённых операцийПример архитектуры:
// UI (часто)
map.on('move', updateUI);
// сеть (средне)
const fetchData = throttle(loadData, 300);
map.on('moveend', fetchData);
// данные (редко)
const updateSource = debounce(() => {
map.getSource('data').setData(bigDataset);
}, 500);
MapLibre может быть приведён к избыточному render loop даже без явных ошибок:
setPaintPropertytriggerRepaintКонтроль осуществляется через: