Основное ограничение при отображении тысяч объектов на карте в Mapbox
GL JS связано не столько с источником данных, сколько с механизмом их
отрисовки. Каждый маркер, созданный через
new mapboxgl.Marker(), представляет собой DOM-элемент, что
приводит к значительным затратам на:
moveПри увеличении количества маркеров до нескольких тысяч наблюдается резкое падение FPS из-за постоянного обновления layout и repaint.
Классический подход с mapboxgl.Marker подходит только
для небольших наборов объектов. Для масштабных данных применяется
рендеринг через слои (Layer API).
Базовая оптимизация заключается в замене маркеров на слой типа
symbol:
map.addSource('points', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: pointsArray
}
});
map.addLayer({
id: 'points-layer',
type: 'symbol',
source: 'points',
layout: {
'icon-image': 'marker-15',
'icon-size': 1,
'icon-allow-overlap': false
}
});
Ключевое преимущество заключается в том, что WebGL рендерит все точки единым проходом без создания отдельных DOM-элементов.
При больших наборах данных (10k–1M точек) даже
symbol layer становится недостаточным. Используется
кластеризация.
Mapbox GL JS поддерживает встроенную кластеризацию на основе алгоритма, аналогичного Supercluster.
map.addSource('points', {
type: 'geojson',
data: geojsonData,
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50
});
Дополнительные слои для кластеров:
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'points',
filter: ['has', 'point_count'],
paint: {
'circle-radius': 18,
'circle-color': '#51bbd6'
}
});
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'points',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-size': 12
}
});
Кластеризация снижает количество отображаемых объектов с десятков тысяч до сотен, что радикально повышает производительность.
При необходимости более гибкого контроля используется
supercluster напрямую.
Основные приёмы:
Пример обработки:
const index = new Supercluster({
radius: 60,
maxZoom: 16
});
index.load(features);
const clusters = index.getClusters([-180, -85, 180, 85], zoom);
Результат подаётся в GeoJSON источник без дополнительных DOM-операций.
Снижение количества рендеримых объектов достигается фильтрацией по текущим границам карты.
const bounds = map.getBounds();
const visibleFeatures = allFeatures.filter(f =>
bounds.contains(f.geometry.coordinates)
);
Обновление источника выполняется только при завершении движения карты.
События move, zoom, rotate
генерируются с высокой частотой. Обновление данных на каждый тик
приводит к перегрузке.
Используется стратегия задержки:
let timeout;
map.on('moveend', () => {
clearTimeout(timeout);
timeout = setTimeout(updateData, 100);
});
Это снижает количество перерасчётов без потери визуальной отзывчивости.
Создание и удаление маркеров или источников данных дорого обходится движку. Вместо этого применяется:
data через setDatasource.setData(updatedGeojson);
Избегается пересоздание слоя, что уменьшает нагрузку на WebGL pipeline.
Каждая уникальная иконка увеличивает нагрузку на GPU и память. Используются спрайты:
icon-image вариантовtext-field в слоях symbol является одной из
самых дорогих операций.
Оптимизации:
text-allow-overlap: false'text-size': [
'interpolate',
['linear'],
['zoom'],
10, 10,
15, 14
]
Фильтрация на уровне GPU позволяет избежать пересчётов Jav * aScript:
filter: ['>', ['get', 'importance'], 5]
или ограничение по масштабу:
minzoom: 5,
maxzoom: 14
Это снижает количество объектов, попадающих в пайплайн рендеринга.
При очень больших объёмах данных GeoJSON становится узким местом.
Vector tiles обеспечивают:
map.addSource('tiles', {
type: 'vector',
url: 'mapbox://examples.tileset-id'
});
При нестандартной визуализации применяется кастомный слой:
Подход позволяет обрабатывать десятки тысяч объектов в одном проходе.
Основной принцип оптимизации WebGL:
Чем меньше переключений состояния, тем выше FPS.
LOD (Level of Detail) снижает нагрузку при отдалении карты:
'circle-radius': [
'interpolate',
['linear'],
['zoom'],
5, 2,
10, 6,
15, 10
]
Типичные источники утечек:
removeSourceonКорректное управление жизненным циклом карты снижает рост памяти при длительной работе интерфейса.
Предварительная обработка данных снижает нагрузку на основной поток:
Разделение вычислений и рендера позволяет стабилизировать FPS при динамическом обновлении данных.