react-native-maps: Custom marker image very slow performance

Is this a bug report?

Yes

Have you read the Installation Instructions?

Yes

Environment

  "dependencies": {
    "expo": "^25.0.0",
    "react": "16.2.0",
    "react-native": "https://github.com/expo/react-native/archive/sdk-25.0.0.tar.gz"
  }

Steps to Reproduce

Markers with default Google Maps marker image show up very quickly, no issues at all.

But when I use a custom image (size is 1kb) it struggles to render.

Expected Behavior

For markers to show up quickly.

Actual Behavior

Markers take too long to show up. For about 200+ markers, it takes around 10-20 seconds.

Reproducible Demo

App.js

import React from 'react';
import { Dimensions } from 'react-native';
import { MapView } from 'expo';
import Marker from './Marker';
import haversine from 'haversine';

const { width, height } = Dimensions.get('window');
const ASPECT_RATIO = width / height;
const LATITUDE = -37.812365;
const LONGITUDE = 144.962338;
const LATITUDE_DELTA = 0.0222;
const LONGITUDE_DELTA = LATITUDE_DELTA * ASPECT_RATIO;

export default class App extends React.Component {
  constructor() {
    super();
    this.state = {
      region: {
        latitude: LATITUDE,
        longitude: LONGITUDE,
        latitudeDelta: LATITUDE_DELTA,
        longitudeDelta: LONGITUDE_DELTA
      },
      data: [],
      isLoaded: false,
      boundingBox: {
        westLng: 0,
        southLat: 0,
        eastLng: 0,
        northLat: 0
      }
    }
  }

  componentDidMount() {
    this._mounted = true;
  }

  componentWillUnmount() {
    this._mounted = false;
  }

  getData = async (region) => {
    const centre = {
      latitude: region.latitude,
      longitude: region.longitude
    }

    const screenVerticalBoundary = {
      latitude: this.state.boundingBox.northLat,
      longitude: region.longitude
    }

    const distance = haversine(centre, screenVerticalBoundary, {unit: 'meter'});
    const url = `http://test.com`;
    let data = await(await fetch(url)).json();
    this.setState({data: data, isLoaded: true});
  }

  onRegionChangeComplete = (region) => {
    let boundingBox = this.getBoundingBox(region);
    this.setState({boundingBox});
    this.getData(region);
  }

  getBoundingBox = (region) => {
    let boundingBox = {
      westLng: region.longitude - region.longitudeDelta/2, // westLng - min lng
      southLat: region.latitude - region.latitudeDelta/2, // southLat - min lat
      eastLng: region.longitude + region.longitudeDelta/2, // eastLng - max lng
      northLat: region.latitude + region.latitudeDelta/2 // northLat - max lat
    }

    return boundingBox;
  }

  isInBoudingBox(coordinate) {
    if (coordinate.latitude > this.state.boundingBox.southLat && coordinate.latitude < this.state.boundingBox.northLat &&
        coordinate.longitude > this.state.boundingBox.westLng && coordinate.longitude < this.state.boundingBox.eastLng)
    {
      return true;
    }
    
    return false;
  }

  render() {
    return (
      <MapView style={ { flex: 1 } } initialRegion={ this.state.region } onRegionChangeComplete={this.onRegionChangeComplete}>
        {this.state.isLoaded ? this.state.data.map(p =>
          this.isInBoudingBox({ latitude: parseFloat(p.lat), longitude: parseFloat(p.lon) }) ? 
          <Marker key={p.st_marker_id} coordinate={ { latitude: parseFloat(p.lat), longitude: parseFloat(p.lon) } } callout="This is a test" />
          : null)
        : console.log('data does not exist yet')}
      </MapView>
      );
  }
}

Marker.js

import React from 'react';
import { MapView } from 'expo';
import { View, Text } from 'react-native';
import markerImage from './assets/marker-128.png';

export default class Marker extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            tracksViewChanges: true
        }
    }

    componentWillReceiveProps(nextProps) {
        if (this.props !== nextProps) {
            this.setState(() => ({
                tracksViewChanges: true,
            }));
        }
    }

    componentDidUpdate() { 
        if (this.state.tracksViewChanges) {
            this.setState({
                tracksViewChanges: false
            });
        }
    }

    render() {
        return (
            <MapView.Marker coordinate={ this.props.coordinate } tracksViewChanges={this.state.tracksViewChanges} image={markerImage}>
                <MapView.Callout>
                    <View>
                        <Text>{this.props.callout}</Text>
                    </View>
                </MapView.Callout>
            </MapView.Marker>
        )
    }
}

I found a few proposed solutions in the repo (adding key, the tracksViewChanges trick, getting rid of funny this.setstate, etc )but none really made any difference.

I am considering using clustering as suggested elsewhere but I would like to try to resolve the issue without having to use it.

Can anyone help please?

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Comments: 22 (3 by maintainers)

Most upvoted comments

@aeskafi It’s not a bug. It’s for updating the marker live, to mimick how it works on iOS. You do not have to comment out code. All you have to do is set tracksViewChanges={false}. If you are loading an image inside the marker, you could start with tracksViewChanges={true}, listen to the load even on your image, and then change it to false.

Make sure to set on the <Marker/> the tracksViewChanges prop to false once all Markers are rendered. For some reason on iOS if you provide a custom marker (Image, View, Svg etc…) it keeps re-rendering it causing the UI thread to go nuts forever.

For me, following this instructions from @heysailor, solved it:

The recommended use of tracksViewChanges is to enable when the marker is being rendered or animated, then to disable it.

An example of use of tracksViewChanges would be to set it to false once onMapReady has fired. This can be accomplished by toggling a state property (e.g. initialized), and passing it to the child map markers in their tracksViewChanges prop. Markers would then render correctly on map load; CPU use falls off once the tracking ceases.

You can find the original quote on the commit he made.

This comment from @davidhellsing was pretty helpful too, and points on the same direction.

@danielgindi huge thanks, I got stuck on this for a while.

I had around 100 markers, that would sometimes completely, partially, or not load (at random). This was on iOS (simulator) with tracksViewChanges={false}.

Upon following your last comment, and creating a custom marker, I’m no longer having this issue, and performance is fixed.

Not sure if this is loosely related to some issues in #578?

This is working for me, in case anyone else finds it of use:

import React, {Component} from 'react';
import {StyleSheet, Image} from 'react-native';
import {Marker} from 'react-native-maps';

export class CustomMarker extends Component
{
    constructor()
    {
        super();
        this.state = {
            tracksViewChanges: true,
        }
    }

    stopRendering = () =>
    {
        this.setState({ tracksViewChanges: false });
    }

    render()
    {
        var marker = this.props.marker;

        return (
            <Marker
                key={marker.key}
                coordinate={marker.coordinates}
                title={marker.title}
                tracksViewChanges={this.state.tracksViewChanges}
            >
                <Image
                    source={marker.image}
                    style={styles.mapMarker}
                    onLoad={this.stopRendering}
                />
            </Marker>
        )
    }

}


const styles = StyleSheet.create({
    mapMarker:
    {
        width: 36,
        height: 50,
    },
});

@limpygnome If you only want a custom image for your marker you probably can fix the performance issues now using the icon property in the marker component which was added in v0.23.0

Did not help me though since my markers also contain badges. My solution was the following

componentDidUpdate() {
    const { tracksViewChanges } = this.state;
    if (tracksViewChanges) {
      setTimeout(() => {
        this.setState({
          tracksViewChanges: false,
        });
      }, 100);
    }
  }

  static getDerivedStateFromProps(props: Props, state: State) {
    const { cachedProps } = state;
    if (!_.isEqual(cachedProps, props)) {
      return {
        tracksViewChanges: true,
        cachedProps: props,
      };
    }
    return {
      tracksViewChanges: false,
      cachedProps: props,
    };
  }

render() {
    const { tracksViewChanges } = this.state;
    const { children } = this.props;
    return (
      <MapView.Marker tracksViewChanges={tracksViewChanges} {...this.props}>
        {children}
      </MapView.Marker>
    );
  }

The timeout however is not very pretty and probably dangerous to use here. But this helped to prevent markers to be initially rendered partially (sometimes). Any ideas why this could happen - without the timeout?

@lucianoacsilva I’d be something like the code below with hooks. Or if you’re using classes then i think you put something of this sort in componentWillUnmount. The point is just store the timerId somewhere and call clearTimeout on it when un-mounting.

const [tracksViewChanges, setViewChanges] = setState();

useEffect(() => {
    let timerId;

    if (tracksViewChanges) {
      timerId = setTimeout(() => {
        setState(false);
      }, 100);
    }
    
    return () => {
      clearTimeout(timerId);
    };
})