Управление памятью

Жизненный цикл карты в Leaflet

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

Жизненный цикл карты начинается с создания экземпляра и заканчивается его полным удалением вместе со всеми связанными DOM-узлами и внутренними слушателями событий. Любые отклонения от этого сценария приводят к накоплению «висящих» ссылок.


Основной объект карты создаётся через L.map, который связывает логическую модель с DOM-контейнером:

const map = L.map('map', {
  center: [51.505, -0.09],
  zoom: 13
});

При создании формируется:

  • DOM-контейнер слоёв
  • внутренняя система координат и тайлов
  • обработчики пользовательских событий (drag, zoom, wheel)
  • менеджер слоёв

Каждый добавленный объект через addTo(map) регистрируется внутри карты и удерживается в памяти через внутренние коллекции.


Утечки памяти в контексте Leaflet

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

  • сохранённые ссылки на карту вне её жизненного цикла
  • не удалённые слои (Layer, TileLayer, Marker)
  • активные тайловые запросы
  • неотписанные события (on)
  • DOM-элементы попапов и tooltip, оставшиеся в документе
  • замыкания в обработчиках событий

Особенно критичны ситуации, когда карта уничтожается визуально, но объект продолжает существовать в памяти:

map = null; // недостаточно

Такая операция не освобождает ресурсы Leaflet.


Удаление карты и корректное освобождение ресурсов

Корректное уничтожение карты выполняется через:

map.remove();

Метод выполняет:

  • удаление DOM-слоёв
  • отключение обработчиков событий карты
  • очистку внутренних коллекций слоёв
  • остановку анимаций и инерции
  • освобождение тайлового контейнера

После вызова remove() ссылка на карту должна быть разорвана:

map.remove();
map = null;

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


Управление слоями (TileLayer, Marker, GeoJSON)

Каждый слой в Leaflet является самостоятельным объектом с жизненным циклом.

TileLayer

const tiles = L.tileLayer(urlTemplate, {
  maxZoom: 19
}).addTo(map);

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

tiles.remove();

Marker

Маркеры удерживаются внутри внутренних структур карты:

const marker = L.marker([51.5, -0.09]).addTo(map);

Удаление:

marker.remove();

или

map.removeLayer(marker);

GeoJSON

GeoJSON-слои часто содержат множество вложенных объектов:

const geo = L.geoJSON(data).addTo(map);

Удаление:

geo.remove();

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


Обработчики событий и их очистка

Leaflet активно использует событийную модель:

map.on('click', onClickHandler);

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

Удаление:

map.off('click', onClickHandler);

При использовании анонимных функций очистка становится затруднённой:

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

Такие обработчики невозможно корректно удалить без ссылки на функцию, что приводит к накоплению утечек при пересоздании карты.


Маркеры и попапы

Попапы и tooltip создают временные DOM-узлы, которые могут сохраняться при неправильном управлении.

marker.bindPopup('Информация');

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

map.closePopup();
marker.remove();

Для tooltip аналогично:

map.closeTooltip();

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


SPA интеграция (React / Vue)

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

Проблемная схема:

useEffect(() => {
  const map = L.map('map');
}, []);

Без cleanup-функции происходит накопление экземпляров карты.

Корректная схема:

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

  return () => {
    map.remove();
  };
}, []);

Дополнительный риск — повторное использование DOM-контейнера. Leaflet сохраняет привязку к DOM-элементу, поэтому повторная инициализация без удаления приводит к ошибкам и утечкам.


Кеширование тайлов и память

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

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

  • переключение между стилями карты без удаления слоя
  • создание новых TileLayer поверх старых
  • динамическое изменение URL без очистки

Корректная последовательность:

oldLayer.remove();
const newLayer = L.tileLayer(newUrl);
newLayer.addTo(map);

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


Практики предотвращения утечек

Управление памятью в Leaflet опирается на дисциплину работы с объектами:

  • всегда удалять слои через removeLayer или remove
  • избегать анонимных обработчиков событий
  • уничтожать карту через map.remove()
  • очищать ссылки на карту и слои после удаления
  • закрывать popups и tooltips перед удалением карты
  • не создавать новые карты в одном контейнере без предварительного уничтожения предыдущей
  • контролировать создание GeoJSON-слоёв при обновлении данных
  • избегать накопления тайловых слоёв при динамическом переключении

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

let isDestroyed = false;

map.on('load', () => {
  if (isDestroyed) return;
  // безопасные операции
});

map.remove();
isDestroyed = true;

Такой подход предотвращает обращение к несуществующим объектам и снижает риск скрытых утечек памяти.