Map Library Instance Memory in Leaflet and Mapbox
Opening the map view ten times makes the tab use a gigabyte more, Chrome warns about too many active WebGL contexts, or Leaflet throws “Map container is already initialized” when a route is revisited. This guide from Third-Party Library Memory Leaks, part of Framework-Specific Memory Optimization, explains what a map instance holds in Leaflet and in WebGL-based libraries such as Mapbox GL JS and MapLibre GL, how to tear it down completely, and when to keep one long-lived map instead.
| Symptom | Root Cause | Immediate Action | Measurable Impact |
|---|---|---|---|
| Memory jumps by 50–150 MB per map view visit | Map instance never removed on unmount | Call map.remove() in the component cleanup |
Memory returns after leaving the view |
| “Too many active WebGL contexts” warning | New GL map per mount, old contexts not released | map.remove(); or keep one map and move it |
Constant number of contexts |
| “Map container is already initialized” (Leaflet) | New L.map on an element that still has a map |
Remove the old map first or reuse it | No duplicate maps |
| Markers and popups retained after data refresh | Marker objects replaced but not removed | marker.remove() / layerGroup.clearLayers() |
Marker count equals data size |
| GPU and tile memory high on mobile | Large tile cache and high-DPI rendering | Bound the tile cache; lower resolution where acceptable | Lower GPU and heap footprint |
Root Cause: A Map Is Many Resources Behind One Element
A Leaflet map (L.map(el)) builds panes and tile <img> elements inside the container, keeps layers (tile layers, markers, vector paths) with their own DOM and event handlers, attaches listeners to the container and to window/document for panning, zooming and resizing, and stores its internal state on the container element (which is why re-initialising the same container throws). map.remove() tears all of that down: it removes layers, unbinds handlers and clears the container’s reference, allowing everything to be collected.
A WebGL map (Mapbox GL JS, MapLibre GL) is heavier. Each map creates a canvas with a WebGL context and allocates GPU textures and buffers for tiles, glyphs and sprites; it parses vector tiles in web workers; it keeps a tile cache of recently used tiles in memory; and it attaches resize and interaction handlers. map.remove() releases the WebGL context (so GPU memory is freed promptly), removes handlers, clears caches and detaches the canvas. Browsers limit the number of simultaneously active WebGL contexts; creating maps without removing old ones eventually makes the browser drop contexts, with blank maps and warnings, as described in WebGL texture and GPU memory leaks.
Two usage patterns cause most map leaks. The first is creating a map per mount without removing it — the framework removes the container element, but the map instance, its listeners, its workers’ message handlers and its GPU resources remain, and the container becomes a detached DOM node retained by the map. The second is replacing overlays instead of updating them: recreating every marker, popup or GeoJSON layer on each data refresh without removing the previous ones. Markers hold DOM elements, event handlers and often closures over application data, so a map with frequent refreshes can accumulate thousands of orphaned markers.
Because maps are expensive to create — style loading, tile fetching, shader compilation — many apps benefit from keeping one map instance for the session and moving its container between views, or hiding it rather than destroying it, as long as that is a deliberate, bounded choice.
Step-by-Step Fix
- Measure a map view’s cost. Enter and leave the map view ten times; record JS heap (after GC), detached DOM and — for WebGL maps — the Task Manager’s GPU memory column. Verification: you know the per-visit growth.
- Remove the map on unmount. Keep the map instance in the component and call
map.remove()in its cleanup. Verification: leaving the view returns heap and GPU memory to baseline; no detached container remains. - Or keep one map deliberately. If creation cost is high and the map appears often, create one map at app level and move or show/hide its container instead of re-creating. Verification: exactly one map instance and one WebGL context exist for the session.
- Manage overlays incrementally. Keep markers in a
Mapkeyed by feature ID; add new ones, update moved ones, and callmarker.remove()for those no longer present. For many points, use a GeoJSON source andsetData()(GL maps) or a canvas/marker cluster layer (Leaflet). Verification: marker object count equals the current data size after refreshes. - Bound caches and resolution. Configure the GL map’s tile cache size where your library exposes it (for example a maximum tile cache option), and avoid rendering at excessive pixel ratios on mobile. Verification: GPU memory stays within your budget during long pans.
- Re-run the ten visits. Repeat step 1. Verification: growth per visit is within noise, and no WebGL context warnings appear.
Command and Code Reference
Use case: a Leaflet map owned by a component (Vue).
// useLeafletMap.js — composable
import { onMounted, onBeforeUnmount, shallowRef } from 'vue';
import L from 'leaflet';
export function useLeafletMap(containerRef, options) {
const map = shallowRef(null); // shallow: don't make Leaflet objects reactive
const markers = new Map(); // featureId → L.Marker
onMounted(() => {
map.value = L.map(containerRef.value, options);
L.tileLayer('https://tiles.example.com/{z}/{x}/{y}.png').addTo(map.value);
});
function syncMarkers(features) {
const seen = new Set();
for (const f of features) {
seen.add(f.id);
const m = markers.get(f.id);
if (m) m.setLatLng(f.latlng);
else markers.set(f.id, L.marker(f.latlng).addTo(map.value));
}
for (const [id, m] of markers) {
if (!seen.has(id)) { m.remove(); markers.delete(id); } // drop stale markers
}
}
onBeforeUnmount(() => {
markers.clear();
map.value?.remove(); // layers, handlers, container state
map.value = null;
});
return { map, syncMarkers };
}
Use case: a GL map with data updates via a source, removed on unmount (React).
function FleetMap({ vehicles }) {
const el = useRef(null);
const map = useRef(null);
useEffect(() => {
map.current = new maplibregl.Map({ container: el.current, style: '/style.json' });
map.current.on('load', () => {
map.current.addSource('vehicles', { type: 'geojson', data: emptyCollection });
map.current.addLayer({ id: 'vehicles', type: 'circle', source: 'vehicles' });
});
return () => { map.current.remove(); map.current = null; }; // WebGL context, workers, caches
}, []);
useEffect(() => {
// Update data in place instead of re-creating markers or the map
map.current?.getSource('vehicles')?.setData(toGeoJson(vehicles));
}, [vehicles]);
return <div ref={el} className="map" />;
}
Verification and Regression Prevention
Verify each map view with repeated visits: heap and GPU memory return to baseline after leaving (or stay constant with a deliberately reused map), detached map containers do not appear in snapshots, no WebGL context warnings are printed, and marker counts equal data sizes after many refreshes. Test on a mid-range phone, where GPU memory limits are reached much sooner than on desktops.
Keep map creation in one hook, composable or service per app so the teardown path cannot be forgotten, and add an end-to-end test that visits the map route ten times and asserts on heap growth. For point-heavy maps, prefer data sources with setData() or clustering over individual DOM markers — it is both faster and far smaller. The general wrapper contract is described in Third-Party Library Memory Leaks.
Edge Cases and Gotchas
Removing a map before it finished loading
Calling remove() while the style is still loading is supported, but callbacks registered on load may still run afterwards in some versions. Guard them with a check that the map instance still exists.
Hidden containers and resize
A map created in a hidden container has zero size and renders incorrectly; a reused map moved between containers needs map.resize() (GL) or map.invalidateSize() (Leaflet) after it becomes visible. Forgetting this leads developers to re-create maps unnecessarily.
Reactive wrappers around map objects
Making map or layer objects deeply reactive (Vue ref, MobX observables) wraps huge internal structures in proxies. Use shallow references for library instances.
Popups with framework content
Popups that render framework components need those components unmounted when the popup closes; removing the popup DOM does not unmount a React root or Vue app mounted inside it.
Frequently Asked Questions
Does Leaflet clean up when the container is removed?
No. The map instance keeps its layers, handlers and state until you call map.remove(). Removing the container element leaves the instance and the detached container in memory, and re-initialising a map on the same element throws.
Why does my Mapbox or MapLibre map use so much GPU memory?
Each map has its own WebGL context with textures and buffers for tiles, glyphs and sprites, sized by the viewport and device pixel ratio. Without map.remove(), each new map adds another context. Remove maps on unmount or reuse one instance.
Should I create a new map on every route visit?
It is the simplest correct approach if you always call map.remove() on leave. If maps are visited often and creation is slow, keeping one map for the session and moving its container can be faster — at the cost of a constant memory footprint.
How should I update many markers?
Avoid removing and re-adding all markers on every update. Keep them keyed by ID and update positions, add new ones and remove missing ones — or, for large numbers, use a GeoJSON source with setData() or a clustering layer so points are rendered without individual DOM elements.
Can multiple maps share workers?
WebGL map libraries typically share a worker pool among maps on the same page, but each map still has its own WebGL context and caches. Multiple simultaneous maps multiply GPU memory; limit how many are live at once.
Related
- Third-Party Library Memory Leaks — the parent topic
- WebGL Texture and GPU Memory Leaks — the GPU side of map memory
- Destroying Chart Instances in Chart.js and ECharts — the same lifecycle for charts
- Framework-Specific Memory Optimization — the section overview