Slow performance in OpenLayers 3 when panning with 500 features


I'm building an application for mapping features onto an image layer depicting a floor plan (using OL's ImageStatic layer). Each feature has an svg icon as style and may have additional svg icons as "badges" around the edges.

I've set up a simplified version of the relevant parts of the code in this jsfiddle.

var map = new ol.Map({
  layers: [],
  interactions: ol.interaction.defaults({}),
  target: "map"
});

var pixelProjection = new ol.proj.Projection({
    code: 'pixel',
    units: 'pixels',
    extent: [0, 0, 4097, 1596]
  }),
  // create layer
  floorMapLayer = new ol.layer.Image({
    source: new ol.source.ImageStatic({
      url: "https://sunriverassistedliving.com/wp-content/uploads/Main-Floor-Plan.jpg",
      imageSize: [4097, 1596],
      projection: pixelProjection,
      imageExtent: pixelProjection.getExtent()
    })
  }),
  // create view
  floorMapView = new ol.View({
    projection: pixelProjection,
    center: [2000, 750] || ol.extent.getCenter(pixelProjection.getExtent()),
    zoom: 1
  }),
  poiSource = new ol.source.Vector({
    features: []
  }),
  vectorLayer = new ol.layer.Vector({
    source: poiSource
  }),
  layerGroup = new ol.layer.Group({
    layers: [floorMapLayer, vectorLayer]
  });

map.setView(floorMapView);
map.setLayerGroup(layerGroup);

var iconStyle = new ol.style.Icon( /** @type {olx.style.IconOptions} */ ({
  anchor: [0.5, 1],
  anchorXUnits: 'fraction',
  anchorYUnits: 'fraction',
  size: [25, 25],
  imageSize: [25, 25],
  src: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmVyc2lvbj0iMS4wIiB3aWR0aD0iMjUiIGhlaWdodD0iMjUiIGlkPSJzdmczNjQ4Ij4KICA8ZGVmcyBpZD0iZGVmczM2NTAiLz4KICA8cGF0aCBkPSJNIDEyLjUxNDksNC45NmUtMDA3IEMgNS42MTE3NCw0Ljk2ZS0wMDcgMCw1LjU4MTk0MDUgMCwxMi40ODUxIEMgMCwxOS4zODgyNiA1LjYxMTc0LDI1IDEyLjUxNDksMjUgQyAxNS4yNzY5NCwyNSAxNy44MDQ4LDI0LjA3NDg5IDE5Ljg3NDg1LDIyLjU1NjYyIEwgMTIuNTQ0NywxNy4xOTMwOSBMIDUuMjE0NTQsMjIuNTg2NDEgTCA4LjA0NTI5LDEzLjk0NTE3IEwgMC42NTU1NCw4LjY3MTA0MDUgTCA5Ljc0MzczOTUsOC42NzEwNDA1IEwgMTIuNDg1MSwwLjAyOTgwMDUgTCAxNS4yODYwNSw4LjY3MTA0MDUgTCAyNC4zNDQ0NTksOC42MTE0NDA1IEwgMTcuMDE0MywxMy45MTUzOCBMIDE5Ljg3NDg1LDIyLjU1NjYyIEMgMjIuOTc4NDc5LDIwLjI4MDI4IDI1LDE2LjYyNjIyIDI1LDEyLjQ4NTEgQyAyNSw1LjU4MTk0MDUgMTkuNDE4MDYsNC45NmUtMDA3IDEyLjUxNDksNC45NmUtMDA3IHogIi8+Cjwvc3ZnPg=="
}));

var styleCache = {};

var customStyleFunctions = [
  function(resolution) {
    var style = new ol.style.Style({
      image: new ol.style.Icon(({
        anchor: [1, 2],
        anchorXUnits: 'fraction',
        anchorYUnits: 'fraction',
        size: [15, 15],
        imageSize: [15, 15],
        src: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmVyc2lvbj0iMS4wIiB3aWR0aD0iMjUiIGhlaWdodD0iMjUiIGlkPSJzdmczNjQ4Ij4KICA8ZGVmcyBpZD0iZGVmczM2NTAiLz4KICA8cGF0aCBkPSJNIDEyLjUxNDksNC45NmUtMDA3IEMgNS42MTE3NCw0Ljk2ZS0wMDcgMCw1LjU4MTk0MDUgMCwxMi40ODUxIEMgMCwxOS4zODgyNiA1LjYxMTc0LDI1IDEyLjUxNDksMjUgQyAxNS4yNzY5NCwyNSAxNy44MDQ4LDI0LjA3NDg5IDE5Ljg3NDg1LDIyLjU1NjYyIEwgMTIuNTQ0NywxNy4xOTMwOSBMIDUuMjE0NTQsMjIuNTg2NDEgTCA4LjA0NTI5LDEzLjk0NTE3IEwgMC42NTU1NCw4LjY3MTA0MDUgTCA5Ljc0MzczOTUsOC42NzEwNDA1IEwgMTIuNDg1MSwwLjAyOTgwMDUgTCAxNS4yODYwNSw4LjY3MTA0MDUgTCAyNC4zNDQ0NTksOC42MTE0NDA1IEwgMTcuMDE0MywxMy45MTUzOCBMIDE5Ljg3NDg1LDIyLjU1NjYyIEMgMjIuOTc4NDc5LDIwLjI4MDI4IDI1LDE2LjYyNjIyIDI1LDEyLjQ4NTEgQyAyNSw1LjU4MTk0MDUgMTkuNDE4MDYsNC45NmUtMDA3IDEyLjUxNDksNC45NmUtMDA3IHogIi8+Cjwvc3ZnPg=="
      }))
    });
    return [style];
  }
];

var defaultStyleFunction = function(resolution) {
  var feature = this;
  this.set('manuallyHidden', false);

  if (!feature.get('selected') && (feature.get('hidden') || feature.get('manuallyHidden'))) {

    // use hidden marker style
    if (!styleCache.hidden) {
      styleCache.hidden = new ol.style.Style({});
    }
    return [styleCache.hidden];
  }

  // draw marker normally
  var iconSrc = iconStyle.getSrc();
  if (!styleCache[iconSrc]) {
    styleCache[iconSrc] = new ol.style.Style({
      image: iconStyle,
    });
  }

  var styles = [styleCache[iconSrc]];

  // add styles from registered overlay style functions
  for (var i = 0; i < customStyleFunctions.length; i++) {
    //console.log(customStyleFunctions[i]);
    styles = styles.concat(customStyleFunctions[i](resolution));
  }

  return styles;
};

for (var i = 0; i < 500; i++) {
  var posX = Math.random() * 4097;
  var posY = Math.random() * 1596;
  var feature = new ol.Feature({
    geometry: new ol.geom.Point([posX, posY]),
  });
  feature.setStyle(defaultStyleFunction);

  poiSource.addFeature(feature);
}

There's 500 features with one badge each. Panning the map feels jerky and the Timeline in Chrome notices that the frame rate drops to around 5 fps.

Chrome Timeline

This is still sort of usable, but in my real application the situation is far worse, with total freeze-ups even when using only around 100 features with 1-2 badges each. I haven't been able to narrow down the reason why my real application is less responsive than this demo, but the profiler doesn't take note of any other code being run than the rendering in OpenLayers and the GPU being busy panting. The bottom layer image in my real app tests is much bigger, though. Around 10000x7000px (the jsfiddle one is around 4000x1600px). This will of course render a huge canvas which will require resources to be re-painted.

I'm wondering if there are any other performance boosts in OpenLayers that I could employ to make panning of the map more responsive? I'm using version 3.15.1 on my late 2011 13-inch MacBook Pro with newest Google Chrome / Firefox.

The static image layer should not be a problem. But you are using style functions for the vector layer in a very inefficient way. Do not create a new style instance with every call of the style function. Instead, create the style outside and only have your style function return it. Also, it is better to set a single style function on the layer, instead of setting one on every feature.


You can use webgl as your rendering option, just add renderer: 'webgl' to your map constructor. I have modified your example right here and used even 10,000 points: https://jsfiddle.net/qmpd04y5/ Unfortunately I had to remove the background floor layer, due to cross origin request failure, this can be of course re-added when the web application runs on a server where the background image has the same origin.

If webgl is not an option you can also set up a WMS system to prerender your features as tile images, which can improve performance by a lot depending on your server. The advantage of using a WMS is that you never have issues of scalability depending on client side, since the client will just load image tiles.