Визуализация карт в Mapbox GL JS основана на использовании WebGL. Несмотря на высокую производительность графического ускорения, каждая операция обновления состояния карты требует затрат вычислительных ресурсов. При работе с большими объёмами данных, множеством слоёв и частыми изменениями источников данных возникает риск появления задержек интерфейса, падения FPS и повышенного потребления памяти.
Основные факторы, влияющие на производительность:
Оптимизация перерисовки направлена на уменьшение количества операций, которые движок должен выполнять между кадрами.
После создания объекта карты запускается цикл визуализации.
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/light-v11',
center: [37.6176, 55.7558],
zoom: 10
});
Перерисовка выполняется при следующих событиях:
Каждый кадр проходит несколько этапов:
Чем меньше изменений происходит между кадрами, тем выше производительность.
Наиболее распространённая причина просадки производительности — постоянное обновление GeoJSON-источников.
Неэффективный вариант:
setInterval(() => {
source.setData(generateLargeGeoJSON());
}, 100);
В этом примере каждые 100 миллисекунд создаётся новый набор данных. Mapbox GL JS вынужден заново анализировать структуру GeoJSON, строить внутренние индексы и пересчитывать отображение объектов.
Особенно критично это становится при работе с десятками тысяч геометрий.
Лучше обновлять данные только при реальном изменении.
function updateData(data) {
if (hasChanges(data)) {
source.setData(data);
}
}
Подобный подход уменьшает количество полных перестроений источника.
При поступлении большого количества событий обновления полезно применять отложенный вызов.
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
}, delay);
};
}
Пример использования:
const updateMapData = debounce((data) => {
source.setData(data);
}, 300);
Если данные приходят десятки раз в секунду, фактическое обновление карты будет происходить значительно реже.
Для анимаций и потоковых данных чаще подходит ограничение частоты вызова.
function throttle(fn, interval) {
let lastCall = 0;
return (...args) => {
const now = Date.now();
if (now - lastCall >= interval) {
lastCall = now;
fn(...args);
}
};
}
Пример:
const updatePosition = throttle((data) => {
source.setData(data);
}, 100);
Карта будет обновляться не чаще десяти раз в секунду.
Каждый слой требует отдельного прохода рендеринга.
Неэффективно:
map.addLayer({
id: 'restaurants',
type: 'circle',
source: 'places'
});
map.addLayer({
id: 'cafes',
type: 'circle',
source: 'places'
});
map.addLayer({
id: 'bars',
type: 'circle',
source: 'places'
});
Более эффективный вариант — объединение категорий в один слой.
map.addLayer({
id: 'places',
type: 'circle',
source: 'places',
paint: {
'circle-color': [
'match',
['get', 'type'],
'restaurant', '#ff0000',
'cafe', '#00ff00',
'bar', '#0000ff',
'#999999'
]
}
});
Количество проходов рендеринга уменьшается, а производительность возрастает.
Каждое выражение вычисляется для большого числа объектов.
Сложное выражение:
[
'case',
['>', ['get', 'population'], 1000000],
'#ff0000',
['>', ['get', 'population'], 500000],
'#ffaa00',
'#00ff00'
]
При тысячах объектов подобные вычисления выполняются огромное количество раз.
Если значения известны заранее, полезно подготовить данные на стороне сервера или при загрузке.
Вместо:
['get', 'population']
можно использовать заранее вычисленное поле:
['get', 'color']
что снижает нагрузку на движок.
Большие GeoJSON-файлы являются серьёзным источником проблем производительности.
Например:
map.addSource('cities', {
type: 'geojson',
data: '/data/cities.geojson'
});
Если файл содержит сотни тысяч объектов, браузеру приходится загружать и хранить весь набор данных.
Гораздо эффективнее использовать векторные тайлы.
map.addSource('cities', {
type: 'vector',
url: 'mapbox://user.cities'
});
Преимущества:
Отображение большого количества маркеров существенно влияет на производительность.
Неоптимальный вариант:
{
"type": "FeatureCollection",
"features": [
// 50000 точек
]
}
Каждая точка требует отдельной отрисовки.
Оптимизированный источник:
map.addSource('points', {
type: 'geojson',
data: data,
cluster: true,
clusterRadius: 50,
clusterMaxZoom: 14
});
Преимущества кластеризации:
HTML-маркеры создаются через DOM.
new mapboxgl.Marker()
.setLngLat([37.61, 55.75])
.addTo(map);
Несколько десятков таких элементов не создают проблем.
Однако тысячи DOM-узлов резко снижают производительность браузера.
Для больших наборов данных рекомендуется использовать слои.
map.addLayer({
id: 'points',
type: 'circle',
source: 'points'
});
Отрисовка через WebGL значительно быстрее.
Часто требуется изменить состояние отдельного объекта.
Неэффективный способ:
source.setData(updatedGeoJSON);
Даже если изменился один объект, обновляется весь источник.
Лучшее решение:
map.setFeatureState(
{
source: 'buildings',
id: 101
},
{
selected: true
}
);
Использование состояния:
'fill-color': [
'case',
['boolean', ['feature-state', 'selected'], false],
'#ff0000',
'#0088ff'
]
В этом случае происходит обновление только нужного объекта.
Частая ошибка — изменение параметров слоя внутри циклов.
Неэффективно:
features.forEach(item => {
map.setPaintProperty(
'points',
'circle-color',
item.color
);
});
Каждый вызов инициирует перестройку слоя.
Лучше выполнять обновление один раз:
map.setPaintProperty(
'points',
'circle-color',
'#ff0000'
);
Либо использовать выражения данных.
Для анимаций следует синхронизировать обновления с циклом рендеринга браузера.
Плохой вариант:
setInterval(update, 16);
Оптимальный вариант:
function animate() {
update();
requestAnimationFrame(animate);
}
animate();
Преимущества:
Большое количество изображений увеличивает объём видеопамяти.
Неэффективно:
map.loadImage('/icons/icon1.png');
map.loadImage('/icons/icon2.png');
map.loadImage('/icons/icon3.png');
При сотнях иконок возникают проблемы:
Эффективнее использовать:
Событие move генерируется непрерывно.
map.on('move', () => {
updateSidebar();
});
Во время перетаскивания карты обработчик может вызываться сотни раз.
Лучше использовать:
map.on('moveend', () => {
updateSidebar();
});
Либо применять throttle.
map.on(
'move',
throttle(updateSidebar, 200)
);
Это значительно уменьшает количество операций между кадрами.
Даже если источник содержит миллионы объектов, отображать следует только необходимые.
Пример фильтрации:
map.setFilter('cities', [
'>',
['get', 'population'],
50000
]);
Или отображение по масштабу:
map.addLayer({
id: 'cities',
type: 'circle',
source: 'cities',
minzoom: 8
});
При малых масштабах слой не будет участвовать в рендеринге.
Mapbox GL JS предоставляет события для контроля процесса отрисовки.
Отслеживание завершения загрузки:
map.on('idle', () => {
console.log('Карта полностью отрисована');
});
Измерение времени обновления:
const start = performance.now();
source.setData(data);
map.once('idle', () => {
console.log(
performance.now() - start
);
});
Подобные измерения позволяют выявлять узкие места в приложении.
Допустимо использование:
Рекомендуется:
Рекомендуется:
Желательно:
source.setData(bigGeoJSON);
Вместо этого:
map.setFeatureState(...);
new mapboxgl.Marker(...)
Вместо этого:
map.addLayer({
type: 'circle'
});
socket.on('message', data => {
source.setData(data);
});
Вместо этого:
socket.on(
'message',
throttle(updateMap, 100)
);
100 отдельных слоёв
Вместо:
1 слой + data-driven styling
map.on('move', heavyCalculation);
Вместо:
map.on('moveend', heavyCalculation);
Грамотная оптимизация перерисовки в Mapbox GL JS строится на уменьшении количества обновляемых данных, сокращении числа слоёв, рациональном использовании WebGL-слоёв вместо DOM-элементов, применении кластеризации, feature-state и векторных тайлов, а также на строгом контроле частоты операций, инициирующих новый цикл рендеринга. Это позволяет поддерживать высокую частоту кадров даже при работе с большими объёмами геоданных и сложными интерактивными картами.