Slow performance in OpenLayers 3 when panning with 500 features
Asked Answered
C

2

6

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.

Cooperstein answered 24/8, 2016 at 14:22 Comment(1)
You are not doing this efficiently. cdn.rawgit.com/ca0v/ol3-lab/v3.17.1/rawgit.html?run=labs/… can have 500 complex styles and render smoothly (in Chrome) on my very slow laptop.Frater
W
13

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.

Wersh answered 29/8, 2016 at 22:6 Comment(4)
Thanks. Setting the style on the layer instead of feature seems feasible when the icons are more or less static. But these icons change a lot and may have many different configurations (icon + badges combinations). It doesn't seem practical to create an own layer for each configuration as opposed to setting styles on the feature itself. Or am I missing something? One optimization I've done is to have a "style cache" where the style objects are stored instead of creating new ones when the configuration changes. I notice now that this practice isn't used for badges, so that's one thing I can do.Phantasm
Also, when I move the features on the map, I want the badges to move along with the feature. Is that even possible if all icons (feature icons and badge icons) of the same type are rendered on their own layer?Phantasm
You may want to familiarise yourself with style functions in OpenLayers. Those are set as property on the vector layer, and are called for each feature when rendering. Style functions are called with the feature and the view resolution as arguments. You can dynamically return different styles based on feature attributes there.Wersh
I'm familiar with style functions, but I currenlty use them individually on each feature. I didn't realize it's possible to set a style function on the layer and that it would be called for each feature. That's definitely something to look in to! Thank you.Phantasm
P
9

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.

Proofread answered 24/8, 2016 at 15:7 Comment(3)
Thanks! Does the WMS system with tile images imply that the features are static, non-interactive? I need the features to be clickable and the icon and badges they use need to change dynamically based on information from a websocket connection.Phantasm
Yes, the displayed features are static and non-interactive at first, but there are some options to make them dynamic. 1. switch layers from a WMS layer to a vector feature layer, when your zoom level is high enough so no input lag appears. 2. Add a feature selection as explained here: gis.stackexchange.com/questions/151979/… and update the tiles, so the selection is also visible. 3. Add an extra layer, loading the features into OpenLayers when they get selected.Proofread
It seems like the webgl renderer doesn't produce a quite as crisp result as the canvas. Also it seemed a bit sluggish when using OSM tiles as background (yes, I'm using those too). Since it worked well in the demo, it leads me to think that there could be some room for optimizing in my code and not only in the canvas rendering.Phantasm

© 2022 - 2024 — McMap. All rights reserved.