Memory leaks

Утечки памяти в приложениях на Leaflet возникают из-за того, что объекты карты, слои и DOM-узлы продолжают существовать в памяти после того, как визуально карта уже удалена или перерисована. Библиотека активно работает с DOM, событиями браузера и внутренними структурами хранения слоёв, поэтому некорректная очистка приводит к накоплению «осиротевших» ссылок.

Основные источники утечек:

  • неотписанные обработчики событий карты и слоёв
  • сохранённые ссылки на экземпляры карты и маркеры
  • таймеры и асинхронные циклы, связанные с обновлением данных
  • плагины, создающие собственные DOM-узлы без последующего удаления
  • неочищенные слои и контейнеры тайлов
  • замыкания, удерживающие объекты карты в памяти

Жизненный цикл карты и точки утечки

Экземпляр карты Leaflet создаётся через L.map() и привязывается к DOM-элементу. При удалении интерфейса приложения этот контейнер может быть уничтожен, но внутренние ссылки Leaflet остаются, если не выполнена явная очистка.

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

Корректный жизненный цикл включает:

  • создание карты
  • добавление слоёв, маркеров, контролов
  • регистрация событий
  • явное удаление через map.remove()
  • очистка сторонних ссылок

Утечки через события

Leaflet активно использует систему событий: click, move, zoom, layeradd, layerremove. Каждый обработчик сохраняется внутри экземпляра карты или слоя.

Типичная проблема

map.on('click', onMapClick);
map.on('moveend', updateState);

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

Правильная очистка

map.off('click', onMapClick);
map.off('moveend', updateState);
map.remove();
map = null;

Особенно критичны анонимные функции:

map.on('click', () => {
  // замыкание удерживает внешние переменные
});

Такие обработчики невозможно снять без сохранения ссылки.


Утечки через маркеры и слои

Маркер (L.marker), полигон (L.polygon) и тайловые слои (L.tileLayer) создают внутренние ссылки на карту и DOM.

Проблемный сценарий

const marker = L.marker([55.75, 37.61]).addTo(map);
markers.push(marker);

Если массив markers сохраняется глобально или в состоянии приложения, объекты не освобождаются даже после удаления карты.

Очистка слоёв

markers.forEach(m => m.remove());
markers.length = 0;

или

map.eachLayer(layer => {
  map.removeLayer(layer);
});

Тайловые слои и кэш изображений

L.tileLayer создаёт большое количество img-элементов. Браузер кэширует изображения, но DOM-узлы могут оставаться связанными с внутренними структурами Leaflet.

Частая проблема

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

const layer = L.tileLayer(url).addTo(map);
// отсутствие layer.remove()

Корректная очистка

map.removeLayer(layer);
layer = null;

Утечки через DOM-контейнер карты

Leaflet использует контейнер div для рендеринга. При удалении SPA-страницы контейнер может оставаться в памяти, если ссылка на карту сохраняется.

Ошибочная модель

const map = L.map('map');
// компонент уничтожен, но map остаётся в замыкании

Даже после удаления DOM:

document.getElementById('map').remove();

объект карты продолжает существовать.

Правильное удаление

map.remove();
map = null;

Замыкания и удержание контекста

Замыкания часто становятся скрытым источником утечек. Любой обработчик Leaflet, использующий внешние переменные, удерживает их в памяти.

function createMap(data) {
  const cache = data;

  const map = L.map('map');

  map.on('click', () => {
    console.log(cache);
  });
}

Переменная cache не освобождается до тех пор, пока жив обработчик.


Таймеры и асинхронные обновления

Leaflet часто используется для динамических данных: обновление маркеров, движение объектов, polling API.

Проблема setInterval

setInterval(() => {
  updateMarkers(map);
}, 1000);

Если карта уничтожена, таймер продолжает работать и удерживает ссылку на map.

Очистка

const id = setInterval(() => {
  updateMarkers(map);
}, 1000);

clearInterval(id);

Geolocation и watchPosition

Браузерный API геолокации часто используется вместе с картами.

navigator.geolocation.watchPosition(pos => {
  marker.setLatLng([pos.coords.latitude, pos.coords.longitude]);
});

Без остановки наблюдения возникает постоянное обновление и удержание callback в памяти.

Очистка

const watchId = navigator.geolocation.watchPosition(callback);

navigator.geolocation.clearWatch(watchId);

Утечки через плагины Leaflet

Плагины часто создают дополнительные слои, DOM-элементы и слушатели событий.

Типичные проблемы:

  • отсутствие метода destroy
  • привязка к глобальному объекту L
  • внутренние setInterval без очистки
  • создание собственных контейнеров поверх карты

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


Утечки в SPA (React, Vue, Angular)

В одностраничных приложениях карта часто создаётся и уничтожается при смене маршрутов.

Основная проблема

DOM удаляется, но объект карты остаётся:

useEffect(() => {
  const map = L.map('map');

  return () => {
    // отсутствие map.remove()
  };
}, []);

Корректный паттерн

useEffect(() => {
  const map = L.map('map');

  return () => {
    map.eachLayer(layer => map.removeLayer(layer));
    map.off();
    map.remove();
  };
}, []);

Ссылки на события мыши и DOM-узлы

Некоторые обработчики Leaflet напрямую привязываются к DOM:

  • popup
  • tooltip
  • кастомные controls

Проблема всплывающих окон

marker.bindPopup('text');

Если маркер не удалён, popup остаётся связанным с DOM-структурами Leaflet.

Очистка

marker.unbindPopup();
marker.remove();

Canvas и SVG рендеринг

Leaflet может использовать SVG или Canvas для отрисовки слоёв. При частом создании/уничтожении карт возникают проблемы:

  • неочищенные SVG-элементы
  • сохранённые ссылки на path
  • canvas-слои с requestAnimationFrame

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

анимационные слои:

function animate() {
  requestAnimationFrame(animate);
}

без отмены цикла при удалении карты.


Глобальные ссылки и кэширование

Утечки часто возникают не в Leaflet, а в архитектуре приложения:

  • сохранение map в глобальном store
  • хранение маркеров в singleton
  • кэширование слоёв без очистки
window.mapInstance = L.map('map');

Такие ссылки делают сборку мусора невозможной.


Практика полной очистки карты

Комплексная деинициализация обычно включает несколько шагов:

function destroyMap(map) {
  map.eachLayer(layer => {
    map.removeLayer(layer);
  });

  map.off();
  map.remove();

  map = null;
}

Дополнительно:

  • очистка таймеров
  • остановка geolocation
  • сброс внешних массивов маркеров
  • удаление DOM-контейнера при необходимости

Детектирование утечек

Основные признаки:

  • рост памяти в DevTools при повторных переходах
  • увеличение числа DOM-узлов .leaflet-*
  • дублирование тайлов
  • замедление интерактивности карты
  • рост количества обработчиков событий

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

  • heap snapshots
  • detached DOM nodes
  • event listener count

Причинно-следственная модель утечек

Утечки в Leaflet почти всегда следуют одной структуре:

  1. создание карты и слоёв
  2. регистрация событий и таймеров
  3. уничтожение DOM без очистки логики
  4. сохранение ссылок в замыканиях или глобальном состоянии
  5. невозможность сборки мусора

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