При разработке картографических приложений на Mapbox GL JS одним из наиболее сложных аспектов становится отображение и обработка больших массивов пространственной информации. Десятки тысяч, сотни тысяч и даже миллионы объектов способны существенно влиять на производительность браузера, скорость отрисовки карты и отзывчивость интерфейса.
Mapbox GL JS изначально проектировался как высокопроизводительный движок визуализации, использующий WebGL для рендеринга данных на видеокарте. Однако даже при аппаратном ускорении эффективность работы зависит от правильной организации данных, выбора источников, структуры слоёв и способов визуализации.
Основные проблемы больших датасетов:
GeoJSON является наиболее популярным форматом для загрузки данных в Mapbox GL JS.
Пример подключения:
map.addSource('cities', {
type: 'geojson',
data: 'cities.geojson'
});
Для небольших наборов данных такой подход удобен и эффективен.
Проблемы начинаются при увеличении количества объектов:
{
"type": "FeatureCollection",
"features": [
...
500000 объектов
...
]
}
Во время загрузки браузер должен:
Каждый этап требует памяти и процессорного времени.
При работе с крупными массивами GeoJSON размер файла становится критическим фактором.
Например:
| Количество объектов | Размер файла |
|---|---|
| 10 000 | 3–5 МБ |
| 100 000 | 30–50 МБ |
| 1 000 000 | 300–500 МБ |
Файлы такого объёма становятся практически непригодными для клиентской загрузки.
Главным инструментом работы с большими наборами данных является технология Vector Tiles.
Вместо передачи полного массива объектов сервер разбивает данные на небольшие части — тайлы.
Схема работы:
Датасет
↓
Генерация тайлов
↓
Запрос нужных тайлов
↓
Отображение только видимой области
Подключение источника:
map.addSource('roads', {
type: 'vector',
url: 'mapbox://examples.roads'
});
Создание слоя:
map.addLayer({
id: 'roads-layer',
type: 'line',
source: 'roads',
'source-layer': 'roads',
paint: {
'line-color': '#1976d2'
}
});
Преимущества:
Именно векторные тайлы являются стандартом для крупных геоинформационных систем.
Для больших корпоративных проектов часто создаются собственные тайловые наборы.
Популярные инструменты:
Пример генерации тайлов через Tippecanoe:
tippecanoe \
-o buildings.mbtiles \
-z14 \
-Z4 \
buildings.geojson
Параметры:
| Опция | Назначение |
|---|---|
| -o | выходной файл |
| -z | максимальный zoom |
| -Z | минимальный zoom |
После генерации тайлы публикуются на сервере.
Подключение:
map.addSource('buildings', {
type: 'vector',
tiles: [
'https://server.com/tiles/{z}/{x}/{y}.pbf'
]
});
При наличии большого количества маркеров карта быстро становится перегруженной.
Пример:
50000 точек
Отображение каждой точки отдельным маркером приводит к серьёзным проблемам производительности.
Mapbox GL JS поддерживает автоматическую кластеризацию.
Источник:
map.addSource('earthquakes', {
type: 'geojson',
data: earthquakes,
cluster: true,
clusterRadius: 50,
clusterMaxZoom: 14
});
Параметры:
| Параметр | Описание |
|---|---|
| cluster | включение кластеризации |
| clusterRadius | радиус объединения |
| clusterMaxZoom | максимальный zoom |
Слой кластеров:
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'earthquakes',
filter: ['has', 'point_count'],
paint: {
'circle-radius': 20,
'circle-color': '#f44336'
}
});
Подпись количества объектов:
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'earthquakes',
filter: ['has', 'point_count'],
layout: {
'text-field': ['get', 'point_count_abbreviated']
}
});
Кластеризация способна уменьшить число отображаемых элементов в сотни раз.
Многие начинающие разработчики используют DOM-маркеры:
new mapboxgl.Marker()
Для нескольких десятков объектов это приемлемо.
Для тысяч элементов такой подход создаёт серьёзную нагрузку на браузер.
Проблема заключается в том, что каждый Marker представляет собой отдельный DOM-узел.
Например:
10000 точек
=
10000 DOM элементов
Гораздо эффективнее использовать слой типа circle.
map.addLayer({
id: 'points',
type: 'circle',
source: 'points',
paint: {
'circle-radius': 4,
'circle-color': '#2196f3'
}
});
В этом случае визуализация полностью переносится на GPU.
Перед публикацией данных рекомендуется минимизировать структуру объектов.
Неэффективный вариант:
{
"id": 15,
"name": "Building A",
"owner": "Company X",
"manager": "John Smith",
"created_at": "...",
"upd ated_at": "...",
...
}
Эффективный вариант:
{
"id": 15,
"name": "Building A"
}
Следует удалять:
Это уменьшает:
Сложные полигоны часто содержат огромное количество вершин.
Пример:
Полигон города
=
150000 вершин
При визуализации большого количества подобных объектов производительность резко падает.
Используется алгоритм упрощения геометрии:
150000 вершин
↓
15000 вершин
Популярные инструменты:
Пример через mapshaper:
mapshaper input.geojson \
-simplify 10% \
-o output.geojson
Снижение количества вершин часто уменьшает размер файла в десятки раз.
Не все объекты должны отображаться на любом уровне приближения.
Например:
Здания
не нужны на масштабе:
zoom = 4
Настройка:
map.addLayer({
id: 'buildings',
type: 'fill',
source: 'buildings',
minzoom: 14
});
Другой пример:
map.addLayer({
id: 'districts',
type: 'fill',
source: 'districts',
maxzoom: 10
});
Такой подход:
Нередко возникает ситуация, когда клиент получает слишком много информации.
Плохой подход:
Сервер → 5 миллионов объектов
Клиент → фильтрация
Правильный подход:
Сервер → 1000 объектов
Клиент → отображение
Запрос:
fetch(
`/api/objects?city=karaganda&type=building`
)
После получения:
source.setData(data);
Основной принцип:
передавать только те данные, которые действительно требуются пользователю.
Большие массивы информации часто загружаются по мере перемещения карты.
Получение текущих границ:
const bounds = map.getBounds();
Формирование запроса:
const bbox = [
bounds.getWest(),
bounds.getSouth(),
bounds.getEast(),
bounds.getNorth()
];
Запрос:
fetch(`/api/features?bbox=${bbox.join(',')}`)
Обновление источника:
source.setData(features);
Подобный механизм используется во многих профессиональных ГИС.
При изменении состояния объектов не следует полностью обновлять GeoJSON.
Неэффективный вариант:
source.setData(updatedGeoJson);
При каждом вызове выполняется повторная обработка всего набора данных.
Лучше использовать Feature State.
Установка состояния:
map.setFeatureState(
{
source: 'objects',
id: featureId
},
{
selected: true
}
);
Использование:
'circle-color': [
'case',
['boolean', ['feature-state', 'selected'], false],
'#ff0000',
'#2196f3'
]
Изменение происходит мгновенно без повторной загрузки данных.
Для больших наборов данных идентификаторы играют важную роль.
Источник:
map.addSource('objects', {
type: 'geojson',
data: data,
promoteId: 'id'
});
Теперь Mapbox GL JS использует поле:
feature.properties.id
как уникальный идентификатор объекта.
Это особенно важно для:
Наличие большого количества обработчиков может ухудшать производительность.
Предпочтительно:
map.on('click', 'points-layer', (e) => {
console.log(e.features[0]);
});
Вместо:
marker.getElement().addEventListener(...)
Первый вариант использует внутренний пространственный индекс Mapbox GL JS и работает значительно быстрее.
При повторных запросах полезно использовать локальный кэш.
Пример:
const cache = new Map();
Сохранение:
cache.se t(tileId, features);
Получение:
if (cache.has(tileId)) {
return cache.get(tileId);
}
Кэширование снижает:
Mapbox GL JS активно использует Web Workers для фоновой обработки данных.
В отдельные потоки выносятся:
Благодаря этому основной поток браузера остаётся отзывчивым даже при обработке больших наборов геоданных.
Однако чрезмерно большие GeoJSON-файлы всё равно создают нагрузку, поэтому наличие воркеров не отменяет необходимость оптимизации данных.
При увеличении объёма данных обычно применяется следующая архитектура:
PostGIS
↓
Генерация Vector Tiles
↓
Tile Server
↓
CDN
↓
Mapbox GL JS
Дополнительно используются:
Кластеризация
+
Упрощение геометрии
+
Фильтрация на сервере
+
Подгрузка по bbox
+
Feature State
Подобная схема позволяет без заметных задержек работать с наборами данных, содержащими миллионы пространственных объектов, сохраняя высокую скорость визуализации и плавность взаимодействия с картой.