Работа с большими датасетами

При разработке картографических приложений на Mapbox GL JS одним из наиболее сложных аспектов становится отображение и обработка больших массивов пространственной информации. Десятки тысяч, сотни тысяч и даже миллионы объектов способны существенно влиять на производительность браузера, скорость отрисовки карты и отзывчивость интерфейса.

Mapbox GL JS изначально проектировался как высокопроизводительный движок визуализации, использующий WebGL для рендеринга данных на видеокарте. Однако даже при аппаратном ускорении эффективность работы зависит от правильной организации данных, выбора источников, структуры слоёв и способов визуализации.

Основные проблемы больших датасетов:

  • длительная загрузка GeoJSON;
  • высокий расход оперативной памяти;
  • падение FPS при перемещении карты;
  • медленная фильтрация объектов;
  • задержки при выполнении запросов;
  • увеличение времени инициализации карты.

Почему GeoJSON становится узким местом

GeoJSON является наиболее популярным форматом для загрузки данных в Mapbox GL JS.

Пример подключения:

map.addSource('cities', {
    type: 'geojson',
    data: 'cities.geojson'
});

Для небольших наборов данных такой подход удобен и эффективен.

Проблемы начинаются при увеличении количества объектов:

{
    "type": "FeatureCollection",
    "features": [
        ...
        500000 объектов
        ...
    ]
}

Во время загрузки браузер должен:

  1. Скачать файл.
  2. Распаковать данные.
  3. Преобразовать JSON в объекты JavaScript.
  4. Передать данные в Mapbox GL JS.
  5. Построить пространственный индекс.
  6. Подготовить буферы для WebGL.

Каждый этап требует памяти и процессорного времени.

При работе с крупными массивами 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;
  • TileServer GL;
  • Tegola;
  • Planetiler;
  • Mapbox Tiling Service.

Пример генерации тайлов через 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']
    }
});

Кластеризация способна уменьшить число отображаемых элементов в сотни раз.


Использование Circle Layer вместо Marker

Многие начинающие разработчики используют 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.


Оптимизация структуры GeoJSON

Перед публикацией данных рекомендуется минимизировать структуру объектов.

Неэффективный вариант:

{
    "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;
  • GDAL;
  • PostGIS;
  • Tippecanoe.

Пример через 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);

Подобный механизм используется во многих профессиональных ГИС.


Использование Feature State

При изменении состояния объектов не следует полностью обновлять GeoJSON.

Неэффективный вариант:

source.setData(updatedGeoJson);

При каждом вызове выполняется повторная обработка всего набора данных.

Лучше использовать Feature State.

Установка состояния:

map.setFeatureState(
    {
        source: 'objects',
        id: featureId
    },
    {
        selected: true
    }
);

Использование:

'circle-color': [
    'case',
    ['boolean', ['feature-state', 'selected'], false],
    '#ff0000',
    '#2196f3'
]

Изменение происходит мгновенно без повторной загрузки данных.


Использование promoteId

Для больших наборов данных идентификаторы играют важную роль.

Источник:

map.addSource('objects', {
    type: 'geojson',
    data: data,
    promoteId: 'id'
});

Теперь Mapbox GL JS использует поле:

feature.properties.id

как уникальный идентификатор объекта.

Это особенно важно для:

  • Feature State;
  • поиска объектов;
  • интерактивных операций;
  • выделения элементов.

Работа с событиями на больших слоях

Наличие большого количества обработчиков может ухудшать производительность.

Предпочтительно:

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);
}

Кэширование снижает:

  • количество сетевых запросов;
  • нагрузку на сервер;
  • время ожидания пользователя.

Использование Web Workers

Mapbox GL JS активно использует Web Workers для фоновой обработки данных.

В отдельные потоки выносятся:

  • парсинг GeoJSON;
  • построение индексов;
  • генерация тайлов;
  • подготовка данных для рендеринга.

Благодаря этому основной поток браузера остаётся отзывчивым даже при обработке больших наборов геоданных.

Однако чрезмерно большие GeoJSON-файлы всё равно создают нагрузку, поэтому наличие воркеров не отменяет необходимость оптимизации данных.


Стратегия масштабирования проектов

При увеличении объёма данных обычно применяется следующая архитектура:

PostGIS
    ↓
Генерация Vector Tiles
    ↓
Tile Server
    ↓
CDN
    ↓
Mapbox GL JS

Дополнительно используются:

Кластеризация
+
Упрощение геометрии
+
Фильтрация на сервере
+
Подгрузка по bbox
+
Feature State

Подобная схема позволяет без заметных задержек работать с наборами данных, содержащими миллионы пространственных объектов, сохраняя высокую скорость визуализации и плавность взаимодействия с картой.