Mario
Mario

Reputation: 968

Slow Performance for filtering markers in react-leaflet

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="&copy; <a href=&quot;http://osm.org/copyright&quot;>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

Answers (1)

Brett DeWoody
Brett DeWoody

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:

  1. Create an array of the markers keys
  2. Loop through the keys, reference the corresponding marker and filter by location using L.latLngBounds().contains()
  3. Loop through the filtered keys to create an array of objects as {key: marker}
  4. Use Object.assign() to create an object from the array of objects

In 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.

The 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

Related Questions