Pattern courant dans une app cartographique Leaflet : un mode temporaire (overlay, sélection, recherche) qui ajoute des markers à un L.layerGroup dédié, puis les retire quand on quitte le mode. Activable/désactivable plusieurs fois pendant la session.
const overlayLayer = L.layerGroup();
let userMarker = null;
let userAccuracyCircle = null;
let connectionLine = null;
function enterMode() {
map.addLayer(overlayLayer);
userMarker = L.marker([lat, lon]).addTo(overlayLayer);
// ... idem pour les autres
}
function exitMode() {
if (userMarker) { userMarker.remove(); userMarker = null; }
if (userAccuracyCircle) { userAccuracyCircle.remove(); userAccuracyCircle = null; }
if (connectionLine) { connectionLine.remove(); connectionLine = null; }
map.removeLayer(overlayLayer);
}
Visuellement à l’écran ça marche : les markers disparaissent quand on exitMode(). Aucune erreur console.
Symptôme
Au bout de quelques cycles enter/exit (3-4 fois), on observe :
- plusieurs user markers empilés au même endroit
- plusieurs cercles « accuracy » qui se superposent
- la ligne de connexion qui se duplique
Le test E2E pour un mode rapide enter/exit/enter révèle le bug : on attend 1 marker au final, on en compte 4.
Cause
marker.remove() (héritée de L.Layer) appelle this._map.removeLayer(this). Ça retire le marker de la carte (du DOM, des panes Leaflet).
Mais ça ne retire pas le marker du LayerGroup qui le contenait.
Le LayerGroup garde une référence interne dans son _layers dict (clé = L.stamp(marker)). Cette référence n’est touchée par aucun marker.remove() ou marker.removeFrom(map). Pour retirer du LayerGroup, il faut explicitement layerGroup.removeLayer(marker).
Conséquence du cycle :
enterMode()→ marker créé, ajouté àoverlayLayer, qui est ajouté à la map → DOM renduexitMode()→marker.remove()retire du DOM mais pas du_layersdeoverlayLayer, puismap.removeLayer(overlayLayer)détache le groupeenterMode()à nouveau →map.addLayer(overlayLayer)ré-attache le groupe avec ses anciens markers fantômes dans_layers, qui se rajoutent au DOM- Un nouveau marker est créé et ajouté → 2 markers en DOM
- Cycles suivants → exponential leak
Le bug est invisible à 1-2 cycles (juste 1 doublon, masqué visuellement par le marker actif). Apparent à partir de 3-4 cycles.
Solution
Utiliser layerGroup.clearLayers() à la sortie du mode. Cette méthode itère le _layers interne et appelle removeLayer pour chaque enfant, ce qui les détache à la fois du groupe et de la map en cascade.
function exitMode() {
overlayLayer.clearLayers(); // ← unique nettoyage, détache tout
userMarker = null;
userAccuracyCircle = null;
connectionLine = null;
map.removeLayer(overlayLayer); // détache le groupe (vide) de la map
}
Plus court ET plus robuste : pas d’oubli quand on ajoute de nouveaux types d’éléments au groupe, tout est nettoyé en une ligne.
Lessons learned
marker.remove()agit sur la map, pas sur leLayerGroupparent — asymétrie discrète : tu add via le LayerGroup, tu remove via la map, le LayerGroup n’est jamais nettoyélayerGroup.clearLayers()est l’idiomatique pour vider un groupe — itère et detach proprement chaque enfant. Pas besoin de tracker chaque marker individuellement si on n’a pas besoin de référence- Le leak est invisible aux premiers cycles parce que les markers s’empilent au même pixel ; il devient visible au 3-4ᵉ enter/exit ou via une assertion E2E type
count === 1 map.removeLayer(overlayLayer)ne vide pas non plus le_layersinterne — il détache juste le groupe de la map, ses contenus restent référencés et reviennent au prochainaddLayer- Diagnostic en 1 ligne :
console.log('overlay layers:', overlayLayer.getLayers().length)à différents moments. Si le count monte de cycle en cycle, c’est ce bug - Pattern général applicable hors Leaflet : quand un objet est tracké par 2 conteneurs (parent + map ici), s’assurer que la suppression touche les 2. Sinon « détaché de A mais visible via B » = leak silencieux. Cf #77 (asymétrie
L.geoJSONenfants vs parent) - Tester le cycle, pas juste l’état stationnaire : faire enter/exit/enter en E2E révèle ce que
entrée + sortie uniquene révèle pas
#leaflet #javascript #frontend #mapping #memory-leak #troubleshooting