【问题标题】:Slow Performance for filtering markers in react-leafletreact-leaflet 中过滤标记的性能缓慢
【发布时间】:2018-12-11 08:11:19
【问题描述】:

我需要一个关于小册子的 react-leaflet 端口的建议。我正在地图上生成标记,并使用带有 react-leaflet-markercluster 的标记聚类。每个标记数据都与一些数据相关联。我想根据视口中的标记过滤该数据。

我的想法:获取地图的边界并与每个标记进行交叉检查。是的,它有效。但是当添加超过 500 个标记时,性能极慢(> 4.5 秒的计算时间)。

我可以做些什么来提高性能?

这是我的代码:

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 将要生成的样本数据量作为输入,并为样本数据创建一个 json 结构 { id: 1 { coordinates: {},...}

瓶颈是函数checkVisibleMarkers。此函数计算标记是否在视口中。 数学上每个标记只有两次乘法。

【问题讨论】:

  • 你能确定代码的哪一部分是瓶颈吗?比如是checkVisibleMarkers函数吗?
  • @BrettDeWoody,是的,完全正确。 checkVisibleMarkers 是瓶颈。此函数计算标记是否在视口中。从数学上讲,每个标记只有两次乘法。
  • 你能设置和分享一个代码笔吗?
  • @Mario 看起来您正在为地图的每次重新渲染创建一个新的 UID(使用 uuid())。这可能意味着正在重新渲染每个标记,这可能会导致您看到的一些性能问题。我已经更新了我的解决方案来解释这一点。

标签: javascript reactjs leaflet maps react-leaflet


【解决方案1】:

我发现了一些潜在问题 - checkVisibleMarkers 函数的性能,使用 uuid() 创建独特(和不同)key每次重新呈现 &lt;Marker /&gt; 时的值。

checkVisibleMarkers

关于checkVisibleMarkers 函数。那里有一些可以优化的调用和模式。以下是目前正在发生的事情:

  1. 创建一个标记键数组
  2. 遍历键,引用相应的标记并使用L.latLngBounds().contains()按位置过滤
  3. 遍历过滤的键以创建对象数组为{key: marker}
  4. 使用Object.assign() 从对象数组中创建一个对象

最后,我们有一个对象,每个值都是一个标记。

我不确定L.latLngBounds 的内部结构,但它可能是造成瓶颈的部分原因。忽略这一点,我将专注于使用 for...in 语句重构 Object.assign({}, ...Object.keys().filter().map()) 模式。

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 });
}

对 jsPerf 的快速检查表明,上述方法比您使用的方法快约 50%,但它不包含 L.latLngBounds().contains() 调用,因此不是精确比较。

我也尝试了一个使用Object.entries(markers).forEach()的方法,比上面的for...in方法稍微慢了一点。

&lt;Marker /&gt;key 属性

&lt;Marker /&gt; 组件中,您使用uuid() 生成唯一键。虽然是唯一的,但每次重新渲染都会生成一个新的键,并且每次组件的键更改时,React 都会创建一个新的组件实例。这意味着在每次重新渲染时都会重新创建每个 &lt;Marker /&gt;

解决方案是为每个&lt;Marker /&gt; 使用唯一且永久的密钥。值得庆幸的是,听起来您已经有了一个可以为此工作的值,即visibleMarkerskey。所以改用这个:

<Marker 
  key={key}
  position={ [latitude, longitude] } 
  icon={icon}    
>
  <Tooltip>{visibleMarkers[key].category}</Tooltip>
</Marker>

【讨论】:

  • 感谢@brett,在性能方面确实不错,可以使用 for-in-loop 而不是 map 过滤器。如果我在地图上有 1000 个标记,它就可以工作。如果我增加到 5k,那肯定会更好,但性能仍然很低。
  • 你能测试L.latLngBounds().contain() 方法,看看这是否是罪魁祸首。例如,用一个简单的函数替换那个条件。您还可以创建对L.latLngBounds() 的引用,因此不会在每次迭代时计算它。比如const bounds = L.latLngBounds([[sW.lat, sW.lng], [nE.lat, nE.lng]]);,然后是const isVisible = bounds.contains([latitude, longitude])
  • FWIW 如果您有很多积分,您可能还想研究使用 RTree 之类的东西来存储您的积分是否有意义。 RTree 在回答诸如“给我这个矩形中存在的所有点”之类的查询方面非常快。
  • @BrettDeWoody,感谢您提供正确方向的提示。我做到了,问题是不同的。随着在地图组件中触发新状态,当 visibleMarkers 更新状态时。地图组件被重新渲染和重新渲染...... n 次(n = visibleMarkers.length)。是的。我猜是一个概念问题。发布解决方案的正确方法是什么?更新我的问题或发布答案并检查?
  • 有趣,您可以发布自己问题的答案并接受它。
猜你喜欢
  • 2021-10-03
  • 2021-10-24
  • 2020-11-12
  • 2018-07-18
  • 2020-03-04
  • 1970-01-01
  • 1970-01-01
  • 2017-11-05
  • 2019-03-09
相关资源
最近更新 更多