Оптимизация множества маркеров

Основное ограничение при отображении тысяч объектов на карте в Mapbox GL JS связано не столько с источником данных, сколько с механизмом их отрисовки. Каждый маркер, созданный через new mapboxgl.Marker(), представляет собой DOM-элемент, что приводит к значительным затратам на:

  • рендеринг и компоновку DOM
  • перерасчёт стилей при перемещении карты
  • обработку событий и пересчёт позиций при каждом move

При увеличении количества маркеров до нескольких тысяч наблюдается резкое падение FPS из-за постоянного обновления layout и repaint.


DOM-маркеры против слоёв карты

Классический подход с mapboxgl.Marker подходит только для небольших наборов объектов. Для масштабных данных применяется рендеринг через слои (Layer API).

DOM-маркеры

  • каждый объект = отдельный DOM-узел
  • высокая гибкость (HTML, события, кастомизация)
  • плохая масштабируемость

Слои Mapbox GL JS

  • рендеринг через WebGL
  • единый источник данных (GeoJSON, vector tiles)
  • минимальное количество draw calls
  • высокая производительность при тысячах и десятках тысяч объектов

Использование GeoJSON и Symbol Layer

Базовая оптимизация заключается в замене маркеров на слой типа 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

При необходимости более гибкого контроля используется supercluster напрямую.

Основные приёмы:

  • предварительная агрегация на клиенте или сервере
  • хранение кластеров по зум-уровням
  • обновление данных только при изменении viewport

Пример обработки:

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

Это снижает количество перерасчётов без потери визуальной отзывчивости.


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

Создание и удаление маркеров или источников данных дорого обходится движку. Вместо этого применяется:

  • повторное использование GeoJSON объектов
  • изменение data через setData
  • кэширование вычисленных кластеров
source.setData(updatedGeojson);

Избегается пересоздание слоя, что уменьшает нагрузку на WebGL pipeline.


Оптимизация иконок и спрайтов

Каждая уникальная иконка увеличивает нагрузку на GPU и память. Используются спрайты:

  • объединение изображений в один atlas
  • минимизация количества icon-image вариантов
  • использование SDF-иконок для масштабирования без потери качества

Текстовые подписи и стоимость layout

text-field в слоях symbol является одной из самых дорогих операций.

Оптимизации:

  • уменьшение длины подписей
  • использование text-allow-overlap: false
  • ограничение отображения по zoom
'text-size': [
  'interpolate',
  ['linear'],
  ['zoom'],
  10, 10,
  15, 14
]

Управление отображением через выражения фильтрации

Фильтрация на уровне GPU позволяет избежать пересчётов Jav * aScript:

filter: ['>', ['get', 'importance'], 5]

или ограничение по масштабу:

minzoom: 5,
maxzoom: 14

Это снижает количество объектов, попадающих в пайплайн рендеринга.


Использование vector tiles вместо GeoJSON

При очень больших объёмах данных GeoJSON становится узким местом.

Vector tiles обеспечивают:

  • загрузку данных по тайлам
  • частичную отрисовку
  • кэширование на уровне браузера
  • масштабирование до миллионов объектов
map.addSource('tiles', {
  type: 'vector',
  url: 'mapbox://examples.tileset-id'
});

Canvas и кастомные WebGL-слои

При нестандартной визуализации применяется кастомный слой:

  • прямое управление WebGL контекстом
  • ручная батч-отрисовка точек
  • минимизация draw calls

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


Снижение количества draw calls

Основной принцип оптимизации WebGL:

  • группировка объектов по типу
  • единые текстуры
  • минимизация смены состояния shader pipeline

Чем меньше переключений состояния, тем выше FPS.


Ограничение детализации при зуме

LOD (Level of Detail) снижает нагрузку при отдалении карты:

  • на малом зуме — кластеры
  • на среднем — агрегированные точки
  • на большом — детальные объекты
'circle-radius': [
  'interpolate',
  ['linear'],
  ['zoom'],
  5, 2,
  10, 6,
  15, 10
]

Работа с памятью и утечки

Типичные источники утечек:

  • не удалённые источники removeSource
  • накопление событий on
  • сохранение ссылок на устаревшие GeoJSON объекты

Корректное управление жизненным циклом карты снижает рост памяти при длительной работе интерфейса.


Асинхронная подготовка данных

Предварительная обработка данных снижает нагрузку на основной поток:

  • кластеризация в Web Worker
  • вычисление bounding boxes заранее
  • подготовка уровней детализации

Разделение вычислений и рендера позволяет стабилизировать FPS при динамическом обновлении данных.