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

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

Корректное завершение работы карты должно включать явное удаление:

map.remove();
map = null;

Метод remove() освобождает:

  • WebGL-контекст
  • тайловый кэш
  • обработчики событий
  • внутренние worker-пулы
  • DOM-узлы canvas

Особенно критично это при SPA-архитектуре, где карта может пересоздаваться при переходах между маршрутами.

Дополнительно важно отслеживать собственные подписки:

function cleanup(map) {
  map.off('click', onClickHandler);
  map.off('move', onMoveHandler);
  map.remove();
}

Утечки памяти через обработчики событий

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

Типичная ошибка:

map.on('move', () => {
  updateUI();
});

Проблема заключается в невозможности снять такой обработчик без ссылки на функцию. Оптимальный подход:

function onMove() {
  updateUI();
}

map.on('move', onMove);
map.off('move', onMove);

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


Контроль источников и слоёв

Каждый source и layer в карте создаёт дополнительные структуры в WebGL и памяти CPU. При динамическом обновлении данных часто происходит накопление старых источников.

Правильная стратегия — явное удаление перед добавлением:

if (map.getSource('points')) {
  map.removeLayer('points-layer');
  map.removeSource('points');
}

Особое внимание требуется GeoJSON-источникам с частыми обновлениями. Вместо пересоздания источника предпочтительно обновление данных:

map.getSource('points').setData(newGeojson);

Это предотвращает фрагментацию памяти и лишние пересборки буферов.


Оптимизация GeoJSON и минимизация объёма данных

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

Практики снижения нагрузки:

  • удаление неиспользуемых properties
  • упрощение геометрии (simplification)
  • переход к векторным тайлам при больших объёмах данных

Пример упрощения данных:

function simplifyFeature(feature) {
  return {
    type: 'Feature',
    geometry: feature.geometry,
    properties: {
      id: feature.properties.id
    }
  };
}

При больших наборах данных предпочтительно использовать серверную генерацию тайлов вместо передачи полного GeoJSON.


Управление кэшированием тайлов

Mapbox GL JS использует встроенный tile cache, который может занимать значительный объём памяти при работе с крупными картами.

Ключевые параметры:

  • ограничение zoom-диапазона (minZoom, maxZoom)
  • использование maxTileCacheSize (если доступно в конфигурации сборки)
  • ограничение viewport через maxBounds

Пример настройки:

const map = new mapboxgl.Map({
  container: 'map',
  style: 'mapbox://styles/mapbox/streets-v12',
  minZoom: 3,
  maxZoom: 16,
  maxBounds: [
    [60, 40],
    [90, 55]
  ]
});

Чем уже диапазон работы карты, тем меньше тайлов остаётся в памяти.


Оптимизация стилей и слоёв

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

  • fill-extrusion слои
  • heatmap слои
  • символы с большим количеством glyphs

Оптимизация достигается:

  • объединением слоёв
  • использованием фильтров вместо множества слоёв
  • минимизацией layout-параметров

Плохая практика:

for (const feature of features) {
  map.addLayer({ id: feature.id, type: 'circle', source: 'points' });
}

Оптимальная стратегия:

map.addLayer({
  id: 'points-layer',
  type: 'circle',
  source: 'points',
  filter: ['==', 'type', 'city']
});

Управление растровыми и векторными режимами

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

При ограниченных ресурсах предпочтительно:

  • использовать raster tiles для базового слоя
  • накладывать векторные слои только для интерактивных объектов

Контроль WebGL-контекста

WebGL-контекст — один из самых чувствительных ресурсов. Его утечка приводит к критическому росту потребления памяти GPU.

Факторы, влияющие на стабильность:

  • наличие нескольких карт одновременно
  • отсутствие map.remove()
  • создание скрытых canvas без уничтожения

Также важно избегать создания карты в скрытых DOM-элементах с частыми пересозданиями.


Работа с изображениями, спрайтами и шрифтами

Sprite-атласы и glyph-растры занимают значительную часть памяти. Их неправильная конфигурация приводит к дублированию загрузок.

Рекомендации:

  • использовать единый sprite для всех стилей
  • избегать динамической подгрузки одинаковых шрифтов
  • ограничивать набор используемых символов

Дебаунсинг и частота обновлений

Частые вызовы setData, setFilter, setLayoutProperty приводят к постоянной переработке буферов.

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

function debounce(fn, delay) {
  let t;
  return (...args) => {
    clearTimeout(t);
    t = setTimeout(() => fn(...args), delay);
  };
}

const updateData = debounce((data) => {
  map.getSource('points').setData(data);
}, 100);

Переиспользование экземпляра карты

Создание новой карты вместо обновления существующей — один из главных источников утечек.

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

  • менять стиль через setStyle
  • обновлять источники, а не пересоздавать карту
  • избегать повторного new Map()
map.setStyle('mapbox://styles/mapbox/dark-v11');

Ограничение сложности рендеринга

Сложность сцены напрямую влияет на потребление памяти GPU:

  • количество вершин
  • количество слоёв
  • прозрачность и blending режимы

При перегрузке сцены полезно:

  • уменьшать детализацию на низких zoom уровнях
  • агрегировать точки (clustering)
  • использовать упрощённые геометрии

Кластеризация как метод снижения нагрузки

Кластеризация уменьшает количество одновременно рендеримых объектов:

map.addSource('points', {
  type: 'geojson',
  data: geojson,
  cluster: true,
  clusterMaxZoom: 12,
  clusterRadius: 50
});

Это снижает:

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

Изоляция тяжелых вычислений

При обработке больших данных важно избегать блокировки основного потока. Mapbox GL JS уже использует Web Workers, но пользовательский код часто нарушает это правило.

Рекомендуется:

  • предварительная обработка данных вне UI
  • перенос вычислений в отдельные workers
  • минимизация трансформаций внутри render-циклов

Контроль утечек через замыкания и кэширование

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

Пример проблемного кода:

function init(map) {
  const hugeDataset = loadData();

  map.on('render', () => {
    process(hugeDataset);
  });
}

Даже после удаления карты hugeDataset остаётся в памяти.

Решение — явное управление областью видимости и очистка ссылок:

let hugeDataset = null;

function init(map) {
  hugeDataset = loadData();

  function handler() {
    process(hugeDataset);
  }

  map.on('render', handler);

  return () => {
    map.off('render', handler);
    hugeDataset = null;
  };
}