I have a map that loads additional markers when the user pans/zooms.
Looking for any ideas here on how I can manage the performance issues as the number of markers grows and each map move results in a re-render of every marker.
What's worse is our search method returns paginated marker results so each map move can result in 1-10 additional re-renders of all markers on the map.
Here is what it looks like....
MyGoogleMap.jsx
import React, {Component, PropTypes} from 'react';
import {withGoogleMap, GoogleMap, Marker, KmlLayer} from 'react-google-maps';
import {maps as gmaps} from 'google';
import {MARKER_CONFIG} from 'map-utils';
class MyGoogleMap extends Component {
constructor(props) {
super(props);
this._markers = {};
this._MARKER_SPRITE_PATH = '/static/main/img/sprites/[email protected]';
this._HOVER_Z_INDEX = 1001;
}
render() {
const {
markers,
zoom,
position,
mapOptions,
onDragStart,
onDragEnd,
onIdle,
onGoogleMapLoaded,
getMarkerLabelText
} = this.props;
const renderedMarkers = (markers && markers.length > 0)
? markers.map(marker => {
const {
spot,
state
} = marker;
const {
spotId,
latitude,
longitude,
activeMarker,
selectedRate,
facility,
title
} = spot;
const markerType = (selectedRate.unavailable)
? 'disabled'
: (activeMarker)
? 'large'
: 'medium';
const icon = {
url: this._MARKER_SPRITE_PATH,
...MARKER_CONFIG[markerType][state].icon
};
const zIndex = (state === 'hover')
? this._HOVER_Z_INDEX
: (activeMarker)
? this._HOVER_Z_INDEX - 1
: null;
const markerText = getMarkerLabelText({selectedRate});
const labelFontSize = (markerText.length > 5)
? `${parseInt(MARKER_CONFIG[markerType][state].label.fontSize.replace('px', ''), 10) - 2}px`
: MARKER_CONFIG[markerType][state].label.fontSize;
return (
<Marker
ref={node => {
this._markers[spotId] = {
node,
markerType
};
}}
key={spotId}
icon={icon}
title={title}
label={
(markerType === 'disabled' && state === 'default')
? ''
: {
text: markerText,
color: MARKER_CONFIG[markerType][state].label.color,
fontSize: labelFontSize,
fontWeight: 'bold',
fontFamily: 'Open Sans,sans-serif'
}
}
position={new gmaps.LatLng(latitude, longitude)}
zIndex={zIndex}
/>
);
})
: null;
return (
<GoogleMap
ref={onGoogleMapLoaded}
zoom={zoom}
defaultCenter={position}
options={mapOptions}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onIdle={onIdle}
>
{renderedMarkers}
</GoogleMap>
);
}
}
MyGoogleMap.propTypes = {
markers: PropTypes.array.isRequired,
zoom: PropTypes.number.isRequired,
position: PropTypes.object.isRequired,
mapOptions: PropTypes.object.isRequired,
onDragStart: PropTypes.func.isRequired,
onDragEnd: PropTypes.func.isRequired,
onIdle: PropTypes.func.isRequired,
onGoogleMapLoaded: PropTypes.func.isRequired,
getMarkerLabelText: PropTypes.func.isRequired
};
export default withGoogleMap(MyGoogleMap);
And the parent render method (the markers come from the state and are updated when the map moves causing a re-render of the MyGoogleMap.jsx)...
return (
<MyGoogleMap
ref={node => { this._map = node; }}
containerElement={
<div
style={{
height: '100%'
}}
/>
}
mapElement={
<div
style={{
height: '100%'
}}
/>
}
markers={markers}
zoom={zoom}
position={position}
mapOptions={this._mapOptions}
onDragStart={this._onDragStart}
onDragEnd={this._onDragEnd}
onIdle={this._onIdle}
onGoogleMapLoaded={this._onGoogleMapLoaded}
getMarkerLabelText={this._getMarkerLabelText}
/>
);
Any help would be greatly appreciated, kinda stumped here. Thanks in advance!
went with native google maps, appreciate the library though for the right use case, keep it up!
hey @bkellgren did you found the native google maps performance better?
@walreyes much but just keep in mind our use case is to only ever add markers to the map as the user browsers so there could be more value in this library for those who need either a pre-defined immutable list of markers OR a smaller overall volume of more dynamic markers being added and removed during the experience
if your needs are to add additional pins to the map as the user browses, my suggestion would be to write your own native wrapper of gmaps api
thanks @bkellgren!
I think you would get much better performance by changing
position={new gmaps.LatLng(latitude, longitude)}
to
position={latLngLiteral}
And then change latitude and longitude In your state to be
latLngLiteral: { lat: latitude, lng: longitude }
You also need to define the arrow function used in ref on the class and pass it in, rather than define it inline, as currently you define the function on every render of the map which is expensive and then pass the new function to <Marker> which again stops it being cached.
You also have a fair amount of logic in your render method that I expect could be worked out else where and set in state, as I expect most of the time it doesn't change between renders.
Lastly you need to add key={spotId} to <Marker> as it is created in an Array.map.
With these changes React should start caching your Markers instead of rerendering them on every state change.
You could also add a shouldComponentUpdate method to MyGoogleMap to set it to only rerender when needed.
I have the same use case as you and so far it is working at a decent speed on my five year old MacBook Air. One other change that might help is to use Overlay instead of Marker As it seems to have less overhead.
Thanks for the suggestions @davidjbradshaw.
Originally we did leverage the Overlay Views over Markers also when using this library and it was much more performant.
Will definitely try this out if/when I come back to this library but moving to a native maps/markers implementation made everything much more sane in our case, allowed me to drop this dependency completely and the performance is looking great.
IMO applying React approaches to elements that do not benefit from the virtual dom (google maps) end up being an extra layer of unneeded complexity with minimal ROI but YMMV
You are right @bkellgren this library could be helpful for a quick mvp but dealing directly with the google maps api is much much better.
I agree with you about using React with Google Maps is not the most efficient way because they have different strategies to render elements.
I ended up using this library because I could not notice soon enough the performance with many markers (10,000+) and writing my own wrapper for google maps was not an option for me at the time, so I ended up tweaking the library to access the google maps api and using react refs to access the markers directly.
Somenone might be in a similiar situation so I will share what I did:
I render the map like this:
const ReactGoogleMap = withScriptjs(withGoogleMap(props => (
<GoogleMap
ref={this.handleMapMounted.bind(this)}
defaultCenter={defaultCenter}
>
{markers.map( (marker) => {
return <MarkerWrapperComponent
key={marker.id}
ref={(m) => {this.handleMarkerRef(m)}}
marker={marker}
/>
})}
</GoogleMap>
)));
I handle the marker component ref with ref={(m) => {this.handleMarkerRef(m)}
In the constructor I initialize an instance object called this.markers = {}
constructor(props) {
super(props);
// Initialize an instance object markers
this.markers = {}
}
Then in handleMarkerRef I store the markers like this:
handleMarkerRef(ref) {
/*
Once the marker component is rendered store it's ref in a hash
The key of the marker it's the marker id
The key gives instant access to the component with it's id
This will be useful to get the marker and change it's properties like icon, visible, etc.
*/
if(ref) {
this.markers[ref.props.marker.id] = ref
}
}
And in the component that renders the map I always return false in shouldComponentUpdate to stop react from rendering everything every time instead I access directly the map api and markers in this method. Something like this:
shouldComponentUpdate(nextProps) {
const selectedMarkers = this.props.selectedMarkers
// Set markers selected
selectedMarkers.forEach((selectedMarker) => {
this.markers[selectedMarker.id].setMarkerSelected()
})
//Set new map center and zoom based on new selected wells
// I needed to tweak `react-google-maps` to give access to setZoom and setCenter of google maps api.
this.map.setZoom(4)
this.map.setCenter(this.getDefaultCenter())
return false
}
And with this I could get a much better performance, if someone is having the same issue I can elaborate more on my solution.
@walreyes when you say that you tweaked react-google-maps to add setZoom and setCenter, do you mean you forked the project? If so, did you publish that anywhere?
Hey, @bmakuh
I actually just setted up the fork, it might be useful for you, take a look: https://github.com/walreyes/react-google-maps.
I did'nt do it because, honestly, I won't use this library for my next project, I'd rather build my own GoogleMapsApi.js wrapper, mount a map in a component, and access the API directly. It's so much more responsive, customizable and better performance.
But this library it's quite useful for a easy map for a quick project.
So you know what I just did: I entered lib/GoogleMap.js and below
panToBounds: function panToBounds(map, args) {
return map.panToBounds.apply(map, (0, _toConsumableArray3.default)(args));
},
I added these three:
setZoom: function setZoom(map, _zoom) {
//For some reason zoom is an array
const zoom = _zoom[0];
return map.setZoom(zoom);
},
setCenter: function setCenter(map, _center) {
//For some reason center is an array
const center = _center[0];
return map.setCenter(center);
},
setControl: function setControl(map, _control) {
//For some reason center is an array
const controlPosition = _control[0];
const control = _control[1];
return map.controls[controlPosition].push(control);
},
And that's it! You can access now those methods through the ref.
I did'nt cleaned up this, because I don't intent to use it later and that's why I did'nt forked it or published it, but here it is.
Let me know if I can help you! @bmakuh
Good luck!
@walreyes thanks for the tip! Working with the Google Maps API was way more of a pain than I envisioned it being, but we got it shipped, so thanks for the help! I appreciate it.
Adding key to MarkerClusterer saved the day.
thanks @ntomkin !
Indeed adding a key to MarkerClusterers seems a lot cheaper to render, especially when you deal with thousands of markers. When my app was sometimes slowing down to a 20-30 seconds render, it's now instant :)
Most helpful comment
Adding
keyto MarkerClusterer saved the day.