Утечки памяти в приложениях на Leaflet возникают из-за того, что объекты карты, слои и 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;
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(() => {
updateMarkers(map);
}, 1000);
Если карта уничтожена, таймер продолжает работать и удерживает ссылку
на map.
const id = setInterval(() => {
updateMarkers(map);
}, 1000);
clearInterval(id);
Браузерный API геолокации часто используется вместе с картами.
navigator.geolocation.watchPosition(pos => {
marker.setLatLng([pos.coords.latitude, pos.coords.longitude]);
});
Без остановки наблюдения возникает постоянное обновление и удержание callback в памяти.
const watchId = navigator.geolocation.watchPosition(callback);
navigator.geolocation.clearWatch(watchId);
Плагины часто создают дополнительные слои, DOM-элементы и слушатели событий.
Типичные проблемы:
destroyLПри использовании плагинов критично учитывать необходимость ручной деинициализации.
В одностраничных приложениях карта часто создаётся и уничтожается при смене маршрутов.
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();
};
}, []);
Некоторые обработчики Leaflet напрямую привязываются к DOM:
popuptooltipmarker.bindPopup('text');
Если маркер не удалён, popup остаётся связанным с DOM-структурами Leaflet.
marker.unbindPopup();
marker.remove();
Leaflet может использовать SVG или Canvas для отрисовки слоёв. При частом создании/уничтожении карт возникают проблемы:
анимационные слои:
function animate() {
requestAnimationFrame(animate);
}
без отмены цикла при удалении карты.
Утечки часто возникают не в Leaflet, а в архитектуре приложения:
map в глобальном storewindow.mapInstance = L.map('map');
Такие ссылки делают сборку мусора невозможной.
Комплексная деинициализация обычно включает несколько шагов:
function destroyMap(map) {
map.eachLayer(layer => {
map.removeLayer(layer);
});
map.off();
map.remove();
map = null;
}
Дополнительно:
Основные признаки:
.leaflet-*Инструменты браузера позволяют отслеживать:
Утечки в Leaflet почти всегда следуют одной структуре:
Ключевая проблема заключается не в самой библиотеке, а в неполном управлении жизненным циклом объектов карты и связанных ресурсов.