Memory snapshots

Внутренняя модель памяти в Mapbox GL JS опирается на несколько уровней: JavaScript-объекты (карта, стиль, источники данных), WebGL-ресурсы (буферы, текстуры, шейдеры), worker-потоки (парсинг тайлов, генерация геометрии) и кэш тайлов. Каждый из этих уровней может становиться источником утечек, если жизненный цикл карты не синхронизирован с освобождением ресурсов.

Жизненный цикл карты и точки удержания памяти

Экземпляр карты удерживает значительное количество связей:

  • стиль (style) содержит слои, источники и состояния рендеринга
  • источники (sources) управляют загрузкой и кэшированием векторных и растровых тайлов
  • слой (layers) создаёт WebGL pipeline для отрисовки
  • кэш тайлов хранит декодированные геометрии и изображения
  • event listeners удерживают ссылки на пользовательский код
  • WebGL контекст удерживает GPU-ресурсы

Ключевой особенностью является то, что значительная часть памяти выходит за пределы GC JavaScript и находится в WebGL/браузерных подсистемах.

WebGL-ресурсы и скрытая память

WebGL-слой формирует объекты, которые не видны напрямую в JavaScript heap:

  • vertex buffer objects (VBO)
  • index buffers
  • texture atlases для символов и иконок
  • framebuffers для offscreen рендеринга
  • compiled shaders

Эти ресурсы освобождаются только при корректном уничтожении контекста. Потеря ссылки в JS не гарантирует освобождение GPU-памяти.

Тайловый кэш и рост потребления памяти

Tile cache — одна из наиболее динамичных частей системы. При перемещении карты:

  • загружаются новые тайлы
  • декодируются vector tile geometry
  • создаются геометрические буферы
  • сохраняются результаты для повторного использования

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

Снимки памяти через DevTools

Основной метод анализа — heap snapshot в Chrome DevTools. При работе с Mapbox GL JS полезно фиксировать три состояния:

  1. сразу после инициализации карты
  2. после загрузки стиля и тайлов
  3. после удаления карты

Разница между этими состояниями позволяет выявить удерживаемые объекты.

Особое внимание:

  • объекты Map
  • внутренние структуры Style
  • массивы Tile
  • closures обработчиков событий

Сценарии утечек памяти

Наиболее типичные проблемы:

1. Не вызван map.remove()

Без явного удаления карты остаются:

  • WebGL context
  • requestAnimationFrame циклы
  • tile workers
  • event listeners
const map = new mapboxgl.Map({ container: 'map' });

// при удалении компонента
map.remove();

2. Утечки через обработчики событий

function onMove() {
  console.log('move');
}

map.on('move', onMove);

// отсутствие cleanup
map.off('move', onMove);

Даже небольшие closures удерживают ссылки на внешние данные.

3. Динамическое добавление слоёв без удаления

map.addLayer({
  id: 'points',
  type: 'circle',
  source: 'points-data'
});

// при повторных рендерах без removeLayer происходит накопление

Снимки состояния карты как объектной модели

Помимо heap snapshot, существует логическая “снимковая” модель состояния карты — сериализация стиля:

const styleSnapshot = map.getStyle();

Этот объект включает:

  • sources
  • layers
  • sprite
  • glyphs
  • metadata

Такой снимок используется для:

  • восстановления UI состояния
  • сравнения конфигураций
  • тестирования регрессий стилей

Однако он не отражает WebGL или worker memory.

Дифференциальный анализ памяти

Методика анализа:

  1. baseline snapshot
  2. выполнение сценария (панорамирование, добавление слоёв)
  3. second snapshot
  4. comparison (retained size)

В DevTools важны показатели:

  • retained size
  • shallow size
  • distance to GC root

Особенно показательны объекты, которые сохраняются после map.remove().

Worker-потоки и скрытые удержания

Mapbox GL JS активно использует Web Workers для:

  • декодирования vector tiles
  • генерации label placements
  • обработки geojson

Worker может удерживать память даже после удаления карты, если:

  • не завершён fetch
  • не завершён decode pipeline
  • остались pending tasks

Это проявляется как “phantom memory”, не видимая в основном heap.

Texture atlas и рост GPU памяти

Текстуры символов и иконок агрегируются в атласы. При динамической смене стиля:

  • загружаются новые glyph sets
  • создаются дополнительные texture pages
  • старые могут оставаться в GPU до сброса контекста

Это объясняет ситуации, когда JavaScript heap стабилен, но использование памяти процесса растёт.

Практика безопасного уничтожения карты

Корректное освобождение ресурсов включает последовательность:

  • удаление event listeners
  • остановка анимаций
  • удаление слоёв и источников (при необходимости)
  • вызов map.remove()
function destroyMap(map) {
  map.getStyle().layers.forEach(layer => {
    if (map.getLayer(layer.id)) {
      map.removeLayer(layer.id);
    }
  });

  Object.keys(map.getStyle().sources).forEach(sourceId => {
    if (map.getSource(sourceId)) {
      map.removeSource(sourceId);
    }
  });

  map.remove();
}

Профилирование и обнаружение аномалий

Инструменты:

  • Chrome Memory tab (Heap snapshot)
  • Performance panel (long tasks, frame drops)
  • performance.memory (ограниченно)
  • WebGL debug extensions

Признаки утечки:

  • рост detached DOM nodes
  • увеличение retained size у Tile объектов
  • повторное создание Style без освобождения предыдущего
  • накопление listeners в closure chains

Стабильность памяти при долгоживущих картах

В устойчивых сценариях карта должна:

  • стабилизировать tile cache
  • переиспользовать buffers
  • ограничивать размер sprite atlas
  • очищать невидимые источники при смене стиля

В противном случае наблюдается линейный рост памяти, особенно при частых setStyle() вызовах.

Снимки памяти как инструмент воспроизводимости

Снимки памяти позволяют фиксировать:

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

В инженерных процессах сравнение snapshot diff становится эквивалентом тестирования состояния рендера, особенно при сложных интерактивных сценах с большим количеством источников и слоёв.