Производительность интерактивной карты напрямую зависит от количества операций рендеринга, выполняемых браузером и графическим процессором. В приложениях на базе Mapbox GL JS карта постоянно реагирует на изменения масштаба, перемещения, обновление данных и модификацию стилей. Каждое подобное действие может инициировать новую перерисовку сцены.
Избыточные перерисовки приводят к следующим последствиям:
Минимизация количества обновлений становится особенно важной при работе с большими объёмами геоданных, сложными слоями и анимациями.
Mapbox GL JS использует WebGL для отображения карты. Внутри библиотеки существует цикл обновления, который запускается при возникновении событий, влияющих на визуальное состояние карты.
Перерисовка может происходить после:
Например:
map.setZoom(12);
После вызова метода карта должна пересчитать положение объектов и выполнить новый рендеринг.
Аналогично:
map.setPaintProperty(
'roads',
'line-color',
'#ff0000'
);
Изменение визуального свойства слоя также приводит к обновлению сцены.
Одной из наиболее распространённых причин снижения производительности является частое обновление GeoJSON-источников.
Источник:
map.addSource('vehicles', {
type: 'geojson',
data: vehiclesData
});
Нежелательный вариант:
setInterval(() => {
source.setData(generateNewData());
}, 100);
В этом случае карта получает новые данные десять раз в секунду и каждый раз пересчитывает:
При большом количестве объектов нагрузка становится значительной.
Лучше группировать обновления:
setInterval(() => {
source.setData(getUpdatedData());
}, 1000);
Снижение частоты обновления в десять раз зачастую практически незаметно визуально, но существенно уменьшает нагрузку.
Часто обновляется весь GeoJSON-файл, хотя изменяется лишь небольшая часть объектов.
Плохо:
source.setData(hugeGeoJson);
Если в коллекции находятся десятки тысяч объектов, Mapbox вынужден заново обработать весь набор данных.
Более эффективный подход заключается в накоплении изменений и обновлении данных пакетами через определённый интервал времени.
Например:
const pendingChanges = [];
function updateVehicle(vehicle) {
pendingChanges.push(vehicle);
}
Далее изменения объединяются в одну операцию обновления.
Такой подход уменьшает количество полных пересчётов источника.
Для динамических визуальных изменений часто используется повторный
вызов setData(), хотя в большинстве случаев достаточно
механизма feature-state.
Предположим, необходимо выделять выбранный объект.
Неэффективный вариант:
source.setData(updatedGeoJson);
Эффективный вариант:
map.setFeatureState(
{
source: 'buildings',
id: 15
},
{
selected: true
}
);
Стиль слоя:
paint: {
'fill-color': [
'case',
['feature-state', 'selected'],
'#ff0000',
'#0080ff'
]
}
Преимущества:
Для интерактивных интерфейсов данный подход считается одним из наиболее эффективных.
Каждый вызов методов изменения стиля инициирует перерасчёт визуального представления слоя.
Например:
map.setPaintProperty(
'buildings',
'fill-opacity',
0.5
);
Если выполнять подобные операции многократно подряд:
map.setPaintProperty(...);
map.setPaintProperty(...);
map.setPaintProperty(...);
map.setPaintProperty(...);
возникает серия последовательных перерасчётов.
Рациональнее объединять изменения логически и выполнять их реже.
Особенно это касается:
setPaintProperty();setLayoutProperty();setFilter();setStyle().Метод setStyle() является одной из самых дорогостоящих
операций в библиотеке.
Пример:
map.setStyle('mapbox://styles/mapbox/dark-v11');
После вызова происходит:
По сути карта строится заново.
Частое переключение стилей во время работы приложения может вызывать заметные задержки.
Если требуется изменить лишь несколько параметров отображения, лучше использовать методы изменения конкретных слоёв.
Каждый слой участвует в процессе рендеринга.
Например:
map.addLayer({
id: 'roads',
type: 'line',
source: 'roads'
});
Если карта содержит сотни слоёв:
Roads
Roads labels
Buildings
Buildings labels
Trees
Trees labels
Water
Water labels
...
увеличиваются:
Рекомендуется объединять данные в меньшем количестве слоёв, когда это возможно.
Например, вместо нескольких однотипных слоёв использовать выражения:
'circle-color': [
'match',
['get', 'type'],
'restaurant', '#ff0000',
'hotel', '#0000ff',
'#00aa00'
]
Один слой обычно работает быстрее нескольких аналогичных слоёв.
Распространённая ошибка выглядит следующим образом:
map.removeLayer('points');
map.addLayer(newLayer);
Подобная операция требует повторной регистрации слоя.
Гораздо эффективнее применять фильтрацию:
map.setFilter(
'points',
['==', ['get', 'category'], 'hotel']
);
В этом случае структура слоя сохраняется, а изменяется только условие отображения.
Каждая анимация вызывает постоянные обновления сцены.
Например:
function animate() {
map.rotateTo(
map.getBearing() + 1,
{ duration: 0 }
);
requestAnimationFrame(animate);
}
animate();
Подобный код инициирует перерисовку практически на каждом кадре.
При наличии большого количества объектов это может привести к существенному падению FPS.
Следует избегать:
События интерфейса могут генерироваться очень часто.
Пример:
input.addEventListener('input', event => {
updateMap(event.target.value);
});
Во время набора текста обновление карты будет происходить после каждого символа.
Лучше использовать debounce:
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
}, delay);
};
}
Использование:
const update = debounce(value => {
updateMap(value);
}, 300);
input.addEventListener('input', e => {
update(e.target.value);
});
Количество обновлений карты сокращается в несколько раз.
Событие движения карты генерируется очень часто:
map.on('move', () => {
updateData();
});
Во время перетаскивания пользователь может вызвать сотни событий за несколько секунд.
Решение — throttle.
function throttle(fn, delay) {
let waiting = false;
return (...args) => {
if (waiting) return;
waiting = true;
fn(...args);
setTimeout(() => {
waiting = false;
}, delay);
};
}
Применение:
map.on(
'move',
throttle(() => {
updateData();
}, 200)
);
Количество дорогостоящих операций значительно уменьшается.
Во многих сценариях обновление данных требуется только после завершения перемещения карты.
Неэффективно:
map.on('move', loadData);
Эффективно:
map.on('moveend', loadData);
Событие возникает единожды после завершения панорамирования.
Аналогично работают:
zoomend
dragend
rotateend
pitchend
Использование конечных событий позволяет избежать большого количества промежуточных обновлений.
Лишние обработчики могут приводить к каскадным обновлениям интерфейса.
Плохо:
map.on('move', updateA);
map.on('move', updateB);
map.on('move', updateC);
map.on('move', updateD);
Каждое событие вызывает четыре отдельных процесса.
Лучше:
map.on('move', () => {
updateA();
updateB();
updateC();
updateD();
});
Централизованная обработка облегчает контроль количества операций.
Часто встречается код:
map.on('move', () => {
const bounds = map.getBounds();
expensiveCalculation(bounds);
});
Если вычисления сложные и параметры не изменились, результат можно сохранить.
Пример:
let lastBounds = null;
map.on('moveend', () => {
const bounds = map.getBounds();
if (
JSON.stringify(bounds) ===
JSON.stringify(lastBounds)
) {
return;
}
lastBounds = bounds;
expensiveCalculation(bounds);
});
Кэширование уменьшает число повторных вычислений и связанных с ними обновлений интерфейса.
Если слой временно не нужен, его можно скрыть.
map.setLayoutProperty(
'traffic',
'visibility',
'none'
);
Это предпочтительнее постоянного удаления и повторного создания:
map.removeLayer('traffic');
map.addLayer(...);
Скрытие позволяет избежать затрат на повторную инициализацию слоя.
Большие GeoJSON-файлы увеличивают время обработки и количество вычислений при обновлении карты.
Например:
{
"type": "FeatureCollection",
"features": [...]
}
При десятках или сотнях тысяч объектов объём данных становится критическим.
Векторные тайлы позволяют:
Для крупных картографических проектов это один из ключевых методов оптимизации.
Минимизация перерисовок невозможна без измерений.
Основные инструменты:
Следует отслеживать:
Типичные признаки избыточных перерисовок:
Профилирование позволяет выявить участки кода, которые инициируют лишние обновления сцены, и оптимизировать их без изменения функциональности приложения.