Leaflet js - memory leak accessing GeoJSON REST Api

Viewed 311

With Leaflet, I need to refresh the features displayed after map movements (pan, zoom).

I access a GeoJSON REST Api thus:

function getFeatures( map, layer, table, user ) {
  var bounds = map.getBounds();
  var style = layer.module.spatial.style || undefined;

  var params = {
    geojson: true,
    bbox: bounds.toBBoxString(),
    srs: 'EPSG:4326',
    limit: 200 // limit returned features
  }; 

  fetch ('api/organisations/' + user.organisation + '/objects/' + table + '?' 
    + new URLSearchParams (params))
  .then( response => response.json())
  .then( data => {
    layer.addData (data)
  })
  .catch( err => {
    console.log("Fetch error on table " + table + ': ' + err);
  });
}

export default function(options, map, L, user) {
  var layer = L.geoJson(null, { /*... styling, popup ...*/ });

  map.on('moveend', function(evt) {
    var map = evt.target;
    getFeatures(map, layer, options._id, user );
  });
}

/*
 * ... in calling script:
 */
map.addLayer(layer);

All works fine, but memory usage (measured in Chrome devtools) rockets steadily upwards, and performance becomes poor.

I imagine that I need to be freeing something, presumably in my "moveend" handler, but have been unable to locate any examples suggesting what I should do.

I've tried map.removeLayer(layer) and map.addLayer(layer) either side of layer.addData (data), this made no difference.

What to do?

2 Answers

inspecting leaflet.js doc, layer seems to have a remove method. But in order to invoke it, you will likely need to get a reference to the geojson layer object. So perhaps something like the following?

function getFeatures( map, layer, table, user ) {
  var bounds = map.getBounds();
  var style = layer.module.spatial.style || undefined;

  var params = {
    geojson: true,
    bbox: bounds.toBBoxString(),
    srs: 'EPSG:4326',
    limit: 200 // limit returned features
  }; 

  fetch ('api/organisations/' + user.organisation + '/objects/' + table + '?' 
    + new URLSearchParams (params))
  .then( response => response.json())
  .then( data => {
    layer.addData (data)
  })
  .catch( err => {
    console.log("Fetch error on table " + table + ': ' + err);
  });
}

let layer // not a huge fan of using var, but I believe you can use var here if you insist

export default function(options, map, L, user) {
  if (!!layer) {
    layer.remove()
  }
  layer = L.geoJson(null, { /*... styling, popup ...*/ });

  map.on('moveend', function(evt) {
    var map = evt.target;
    getFeatures(map, layer, options._id, user );
  });
}

/*
 * ... in calling script:
 */
map.addLayer(layer);

You may need to do some debouncing, since getFeatures etc is an async call, and you could have some race condition shenanigans.

Here's how my code ended up, thanks to @pandamakes answer:

let layer;

function getFeatures( map, L, options, user ) { // Called on every map pan/zoom
  var table = options._id;
  if (!! layer) {
    layer.remove();
  }

  if(options.spatial.geometry == 'polygon') {
    layer = L.geoJson(null, {
      style: function (feature) {
        return feature.style || undefined;
      },
      onEachFeature: function (feature, layer) {
        layer.bindPopup(feature.properties.plats_namn); 
      }
    });
  } else {
    layer = L.geoJson(null, {
      pointToLayer: function (feature, latlng) {
        return L.circleMarker(latlng, feature.style || undefined);
      },
      onEachFeature: function (feature, layer) {
        layer.bindPopup(feature.properties.art);
      }
    });
  }

  /* 
   * Set the module on the source for filtering 
   */
  layer.module = options;
  layer.name = name;

    var bounds = map.getBounds();
    var style = layer.module.spatial.style || undefined;

    var params = {
      geojson: true,
      bbox: bounds.toBBoxString(),
      srs: 'EPSG:4326'
      style: style,
      limit: 200
    }; 

    fetch ('api/organisations/' + user.organisation + '/objects/' + table + '?' 
      + new URLSearchParams (params))
    .then( response => response.json())
    .then( data => {
      layer.addData (data)
      map.addLayer(layer)
    })
    .catch( err => {
console.log("Fetch error on table " + table + ': ' + err);
    });
}

export default function(options, map, L, user) {
  map.on('moveend', function(evt) {
    var map = evt.target;
    getFeatures(map, L, options, user );
  });
}

Unsatisfactory, but memory leak gone ...

It would be preferable to simply remove the existing features on the layer on every pan/zoom; I shall look into the possibilities of doing this.

Related