Recommended approach for displaying a map with many markers (500+), one of which is updated very frequently (60Hz)? #1306
-
Recommended approach for displaying a map with many markers (500+), one of which is updated very frequently (60Hz)? Hello, I'm encountering a performance issue and I'm not sure it's due to a limitation of the library or in my way of using it. I'm rebuilding my app in Flutter, and one of the key UI flows is to allow the user to place polygons and polylines on the map. To achieve that, I show a mix of markers and polygons/polylines that are updated in real time on the map as the the user is moving the map camera. I'm getting very bad performance if there are already many markers on the map, and my guess is that the map view is doing extra work to recompute all the markers when it should only compute changes for a few markers. I think this use case could come up not just in my app, so I'm simplifying the problem to make it easier to find the first step towards resolving this. Simplified problem statement: let's say I want a map view that has many markers, but only one marker's location is updated each time I move the map camera, 60 times per second, like so: Attempt 1 Keeping the layer containing the marker having changes, I assume a correct way to implement this would be the following code: class FlutterMapExample1 extends StatefulWidget {
const FlutterMapExample1({Key? key}) : super(key: key);
@override
State<FlutterMapExample1> createState() => _FlutterMapExample1State();
}
class _FlutterMapExample1State extends State<FlutterMapExample1> {
final List<Marker> manyStaticMarkers = [];
final List<Marker> oneMarkerChangingOften = [];
int counter = 0;
Future<void> _incrementCounter() async {
await Future.delayed(Duration.zero);
setState(() {
counter++;
final double latAndLng = (counter % 1000 - 500) / 1000;
oneMarkerChangingOften.clear();
oneMarkerChangingOften.add(_buildMarker(
Colors.red,
position: LatLng(latAndLng, latAndLng),
));
});
}
static Marker _buildMarker(final Color color, {final LatLng? position}) {
return Marker(
point: position ?? LatLng(0, 0),
builder: (_) => Container(
color: color,
width: 24,
height: 24,
),
);
}
@override
void initState() {
for (int i = 0; i < 500; i++) {
manyStaticMarkers.add(_buildMarker(Colors.green));
}
_incrementCounter();
super.initState();
}
@override
Widget build(BuildContext context) {
return FlutterMap(
options: MapOptions(
center: LatLng(0, 0),
zoom: 7,
onPositionChanged: (_, __) => _incrementCounter(),
),
layers: <LayerOptions>[
TileLayerOptions(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
minNativeZoom: 1,
maxNativeZoom: 19,
maxZoom: 21,
),
MarkerLayerOptions(markers: manyStaticMarkers),
MarkerLayerOptions(markers: oneMarkerChangingOften),
],
);
}
} The performances I'm measuring with this example are the following:
After fiddling with this, I realized it doesn't matter that I'm changing that marker data, what is actually causing the performance hit is the call to Future<void> _incrementCounter() async {
await Future.delayed(Duration.zero);
setState(() {
counter++;
});
} So the conclusion I'm drawing from this is that if I expect to want to update some map data at a high rate, I need to find another way to update the data than using Attempt 4 (yes it took me a few tries to get there) After playing around with the code and specifically the LayerOption objects, I was able to achieve what I'm trying to do, with no impact on performance (it's just one marker, changing 60 times per second), here is the code: class FlutterMapExample4 extends StatefulWidget {
const FlutterMapExample4({Key? key}) : super(key: key);
@override
State<FlutterMapExample4> createState() => _FlutterMapExample4State();
}
class _FlutterMapExample4State extends State<FlutterMapExample4> {
// Defining a stream controller that will be used to notify about rebuild needed of the frequentUpdatesLayerOption
final StreamController<void> streamController = StreamController<void>();
late final MarkerLayerOptions frequentUpdatesLayerOption = MarkerLayerOptions(
markers: [],
// Passing the StreamController's stream to the MarkerLayerOptions
rebuild: streamController.stream.asBroadcastStream(),
// Setting usePxCache to false because the StreamController's stream seems to be ignored otherwise
usePxCache: false,
);
final List<LayerOptions> layers = <LayerOptions>[];
int counter = 0;
Future<void> _incrementCounter() async {
counter++;
final double latAndLng = (counter % 1000 - 500) / 1000;
// Updating the marker layer
frequentUpdatesLayerOption.markers.clear();
frequentUpdatesLayerOption.markers.add(_buildMarker(
Colors.red,
position: LatLng(latAndLng, latAndLng),
));
// Sending something in the stream to notify the frequentUpdatesLayerOption to rebuild
streamController.add(null);
}
static Marker _buildMarker(final Color color, {final LatLng? position}) {
return Marker(
point: position ?? LatLng(0, 0),
builder: (_) => Container(
color: color,
width: 24,
height: 24,
),
);
}
@override
void initState() {
final List<Marker> staticMarkers = [];
for (int i = 0; i < 500; i++) {
staticMarkers.add(_buildMarker(Colors.green));
}
_incrementCounter();
layers.add(TileLayerOptions(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
minNativeZoom: 1,
maxNativeZoom: 19,
maxZoom: 21,
));
layers.add(MarkerLayerOptions(markers: staticMarkers));
layers.add(frequentUpdatesLayerOption);
super.initState();
}
@override
Widget build(BuildContext context) {
return FlutterMap(
options: MapOptions(
center: LatLng(0, 0),
zoom: 7,
onPositionChanged: (_, __) => _incrementCounter(),
),
layers: layers,
);
}
} In conclusion this "works" for what I'm trying to achieve, but I'm wondering if it would be possible to achieve this with If anybody has any insights on this, I'd love to read about it, thanks in advance! |
Beta Was this translation helpful? Give feedback.
Replies: 7 comments 8 replies
-
Good question and nice clear examples :). I haven't got a lot of time to dig atm, just had a quick test and all ran at exactly the same speed for me, but that may well be my device I suspect. It's not quite clear if the updating marker will get location updates async and separate to any map interaction or not (and if just movement is a way of testing it or not) ? Does it really get location updates at 60hz (that feels quite heavy for mobile battery use if its going to update at that whatever, but I suspect I've the wrong end of the stick..and its more about visual performance) ? |
Beta Was this translation helpful? Give feedback.
-
Maybe I should set my custom status to 'Ask me about Thanks for this example, might come in useful in future. I was wondering if the performance might be better if you used Attempt 1, but wrapped the changing layer in a In terms of |
Beta Was this translation helpful? Give feedback.
-
I think I'm missing something with this thread :D, it is early in the morning though! If you are dragging the map about, you are moving all the other markers around. I don't really understand why the need for the setState in the first example. It works just fine without all the async stuff and the same performance as the stream example, and same as example below, but see if Jaffas stream addition to that makes a difference. So I suspect I'm still missing the point :D. This is what I'd tried earlier...
|
Beta Was this translation helpful? Give feedback.
-
Does version 1 still work though out of interest (performance wise and update wise) if you remove setState (and set pxCache to false) ? |
Beta Was this translation helpful? Give feedback.
-
Well I guess the result of this then is: if you don't have an external source and just need to move based on map movement, use @ibrierley's probably simpler answer; otherwise, stream in the external source as directly to the concerned layer as possible. All in all, avoid rebuilding the whole map: we kind of knew this part already, but the rest is useful info. |
Beta Was this translation helpful? Give feedback.
-
I was just about to write similar to that. This is a bit of an unusual case as it's kind of already tied to a stream behind the scenes (based on map movement and all markers need repositioning), so I was a bit wary of overcomplicating it if necessary. If there's a gps stream or something, it makes more sense to go the other route (which is more typical, but also why there's not necessarily a singular recommended approach I think. I do think we should have a recommendation to use Widgets in general rather than Layers, and maybe update examples with them, if only because I think they are simpler and more intuitive, and I'd like to see all the older layer code vanish one day :D. |
Beta Was this translation helpful? Give feedback.
-
@ibrierley @JaffaKetchup thanks for your help with this! |
Beta Was this translation helpful? Give feedback.
Well I guess the result of this then is: if you don't have an external source and just need to move based on map movement, use @ibrierley's probably simpler answer; otherwise, stream in the external source as directly to the concerned layer as possible.
All in all, avoid rebuilding the whole map: we kind of knew this part already, but the rest is useful info.