Reputation: 968
I need an advice for the react-leaftlet port of leaflet. I am generating markers on a map and use marker clustering with react-leaflet-markercluster. Each markerdata is associated with some data. I want to filter that data based on the markers in the viewport.
My idea: Get the boundaries of the map and cross-check with each markers. Yes, it works. But the performance is extremly slow (> 4.5secs for calculating), when adding more than 500 markers.
What can I do to increase the performance?
Here is my code:
import React, { Component, Fragment } from 'react';
import CustomMarkers from './components/CustomMarkers';
import { Map, TileLayer } from 'react-leaflet';
import ImageContainer from './components/ImageContainer';
import { checkIfMarkerOnMap, createSampleData } from './utils/helpers';
import L from 'leaflet';
class App extends Component {
constructor(props){
super(props);
this.state = {
viewport: {
width: '100%',
height: '400px',
latitude: 40.00,
longitude: 20.00,
zoom: 5
},
visibleMarkers: {},
markers : {},
}
}
componentDidMount = () => {
const sampleData = createSampleData(1000);
this.setState({ markers: sampleData, visibleMarkers: sampleData });
const mapBoundaries = this.mapRef.contextValue.map.getBounds();
this.setState({ mapBoundaries });
}
getMapBoundaries = () => {
// Get map boundaries
const mapBoundaries = this.mapRef.contextValue.map.getBounds();
if(this.state.mapBoundaries !== mapBoundaries){
console.log("different");
this.setState({ mapBoundaries } );
} else return;
}
checkVisibleMarkers = () => {
console.time("checkVisibleMarkers");
const { markers, mapBoundaries } = this.state;
let visibleMarkers = Object.keys(markers)
.filter(key => (L.latLngBounds([[mapBoundaries._southWest.lat, mapBoundaries._southWest.lng], [mapBoundaries._northEast.lat, mapBoundaries._northEast.lng]]).contains([markers[key].coordinates.latitude,markers[key].coordinates.longitude])))
.map(key => { return { [key] : markers[key] } });
visibleMarkers = Object.assign({}, ...visibleMarkers);
console.log("visibleMarkers", visibleMarkers);
// this.setState({ visibleMarkers })
console.timeEnd("checkVisibleMarkers");
}
handleViewportChanged = () => {
this.getMapBoundaries();
this.checkVisibleMarkers();
}
render() {
console.log("this.mapRef", this.mapRef);
const { viewport, markers, visibleMarkers } = this.state;
const position = [viewport.latitude, viewport.longitude]
return (
<Fragment>
<Map
ref={(ref) => { this.mapRef = ref }}
center={position}
zoom={viewport.zoom}
maxZoom={15}
onViewportChanged={() => this.handleViewportChanged()}
style={{ height: '400px' }}>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution="© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors"
/>
<CustomMarkers visibleMarkers={markers} />
</Map>
{/* <ImageContainer visibleMarkers={visibleMarkers} /> */}
</Fragment>
)
}
}
export default App;
CustomMarker.js:
import React, { Component } from 'react';
import { Marker, Tooltip } from 'react-leaflet';
import uuid from 'uuid-v4';
import {
heartIcon,
heartIconYellow,
heartIconLightblue,
heartIconDarkblue } from './../icons/icons';
import MarkerClusterGroup from 'react-leaflet-markercluster';
import L from 'leaflet';
class CustomMarkers extends Component {
render() {
const { visibleMarkers } = this.props;
let markers;
if(Object.keys(visibleMarkers).length > 0) {
markers = Object.keys(visibleMarkers).map(key => {
let latitude = visibleMarkers[key].coordinates.latitude;
let longitude = visibleMarkers[key].coordinates.longitude;
let icon = heartIcon;
if(visibleMarkers[key].category === 'fb') icon = heartIconLightblue;
if(visibleMarkers[key].category === 'blogs') icon = heartIconYellow;
if(visibleMarkers[key].category === 'artisan') icon = heartIcon;
if(visibleMarkers[key].category === 'website') icon = heartIconDarkblue;
return (
<Marker
key={uuid()}
position={ [latitude, longitude] }
icon={icon}
>
<Tooltip>{visibleMarkers[key].category}</Tooltip>
</Marker>
)
});
}
const createClusterCustomIcon = (cluster) => {
return L.divIcon({
html: `<span>${cluster.getChildCount()}</span>`,
className: 'marker-cluster-custom',
iconSize: L.point(40, 40, true),
});
}
return (
<MarkerClusterGroup
iconCreateFunction={createClusterCustomIcon}
disableClusteringAtZoom={10}
zoomToBoundsOnClick={true}
spiderfyOnMaxZoom={false}
removeOutsideVisibleBounds={true}
maxClusterRadius={150}
showCoverageOnHover={false}
>
{markers}
</MarkerClusterGroup>
)
}
}
export default CustomMarkers;
createSampleData takes the amount of sample data to generate as an input and creates a json structure for sample data { id: 1 { coordinates: {},...}
The bottleneck is the function checkVisibleMarkers. This function calculates if the marker is in viewport. Mathimatically its just two multiplications per marker.
Upvotes: 1
Views: 3125
Reputation: 62851
I see a few potential issues - the performance of the checkVisibleMarkers
function, and the use of uuid()
to create unique (and different) key
values on each rerender of a <Marker />
.
checkVisibleMarkers
Regarding the checkVisibleMarkers
function. There's a few calls and patterns in there which could be optimized. Here's what's currently happening:
L.latLngBounds().contains()
{key: marker}
Object.assign()
to create an object from the array of objectsIn the end, we have an object with each value being a marker.
I'm unsure of the internals of L.latLngBounds
but it could be partly responsible for the bottleneck. Ignoring that, I'll focus on refactoring the Object.assign({}, ...Object.keys().filter().map())
pattern using a for...in
statement.
checkVisibleMarkers = () => {
const visibleMarkers = {};
const { markers, mapBoundaries } = this.state;
for (let key in markers) {
const marker = markers[key];
const { latitude, longitude } = marker.coordinates;
const isVisible = mapBoundaries.contains([latitude, longitude]);
if (isVisible) {
visibleMarkers[key] = marker;
}
}
this.setState({ visibleMarkers });
}
A quick check on jsPerf shows the above method is ~50% faster than the method you're using, but it doesn't contain the L.latLngBounds().contains()
call so it's not an exact comparison.
I also tried a method using Object.entries(markers).forEach()
, which was slightly slower than the for...in
method above.
key
prop of <Marker />
In the <Marker />
component you're using uuid()
to generate unique keys. While unique, each rerender is generating a new key, and every time a component's key changes, React will create a new component instance. This means every <Marker />
is being recreated on every rerender.
The solution is to use a unique, and permanent, key for each <Marker />
. Thankfully it sounds like you already have a value that will work for this, the key
of visibleMarkers
. So use this instead:
<Marker
key={key}
position={ [latitude, longitude] }
icon={icon}
>
<Tooltip>{visibleMarkers[key].category}</Tooltip>
</Marker>
Upvotes: 1