【问题标题】:Performance issues with 1k+ markers with popups in React LeafletReact Leaflet 中带有弹出窗口的 1k+ 标记的性能问题
【发布时间】:2020-11-12 03:09:43
【问题描述】:

我有一个带有 React Leaflet 库的 React 应用程序,我在一个小镇的地图中为每个建筑物显示一个标记。我总共有大约 5k 个标记和一个只显示我想要的标记的过滤器。

但是,我注意到下面的代码对我的性能造成了巨大的影响。我已经研究了一些替代方案,例如 PixiOverlay 和标记聚类,但是将当前代码库迁移到前者相当复杂,而后者根本无法解决我的问题。

我当前的代码:

import React, {
    useRef, useEffect, useContext, useState,
} from 'react';
import ReactDOMServer from 'react-dom/server';
import {
    Marker, useLeaflet, Popup, Tooltip, CircleMarker, Circle,
} from 'react-leaflet';

import L from 'leaflet';
import styled from 'styled-components';

interface IProps {
    coords: [number, number]
    description: string,
    name: string
}

let timeoutPopupRef: any = null;
let timeoutPopupRefClose: any = null;

const DynamicMarker: React.FC<IProps> = ({ coords, description, name }) => {
    const markerRef = useRef<any>(null);
    const popupRef = useRef<Popup>(null);
    const tooltipRef = useRef<Tooltip>(null);
    const leaflet = useLeaflet();

    const divIcon: L.DivIcon = L.divIcon({
        iconSize: [25, 25],
        className: 'marker-white',
    });

    const onComponentMount = () => {
        if (!leaflet.map) return;
        if (!markerRef.current) return;

        const mapZoom: number = leaflet.map.getZoom();

        if (popupRef.current) {
            if (mapZoom <= 17) {
                markerRef.current.leafletElement.unbindPopup();
            } else if (mapZoom > 17) {
                markerRef.current.leafletElement.bindPopup(popupRef.current!.leafletElement);
            }
        }

        if (tooltipRef.current) {
            if (mapZoom <= 15) {
                markerRef.current.leafletElement.unbindTooltip();
            } else if (mapZoom > 15) {
                markerRef.current.leafletElement.bindTooltip(tooltipRef.current!.leafletElement);
            }
        }

        leaflet.map!.on('zoomend', onMapZoomEnd);
    };
    useEffect(onComponentMount, []);

    const onMapZoomEnd = () => {
        if (!markerRef.current) return;
        if (!popupRef.current) return;
        if (!leaflet.map) return;

        const zoom = leaflet.map.getZoom();

        if (zoom < 17) {
            if (!markerRef.current!.leafletElement.isPopupOpen()) {
                markerRef.current!.leafletElement.unbindPopup();
            }
        } else if (zoom >= 17) {
            markerRef.current!.leafletElement.bindPopup(popupRef.current.leafletElement);
        }
    };

    const handlePopupVisible = (value: boolean) => {
        if (!markerRef.current) return;
        if (timeoutPopupRefClose) clearTimeout(timeoutPopupRefClose);

        if (value) {
            if (!markerRef.current!.leafletElement.isPopupOpen()) {
                timeoutPopupRef = setTimeout(() => {
                markerRef.current!.leafletElement.openPopup();
                }, 400);
            }
        } else {
            if (timeoutPopupRef) {
                clearTimeout(timeoutPopupRef);
            }

            if (markerRef.current!.leafletElement.isPopupOpen()) {
                timeoutPopupRefClose = setTimeout(() => {
                markerRef.current!.leafletElement.closePopup();
                }, 100);
            }
        }
    };

    const onComponentDismount = () => {
        leaflet.map!.off('zoomend', onMapZoomEnd);

        if (!markerRef.current) return;
        markerRef.current.leafletElement.remove();
    };
    useEffect(() => onComponentDismount, []);

    return (
        <Marker
            icon={divIcon}
            position={coords}
            onmouseover={() => handlePopupVisible(true)}
            onmouseout={() => handlePopupVisible(false)}
            ref={markerRef}
        >
            <Popup className="custom-popup-content" ref={popupRef} closeButton={false}>
                <div
                    onMouseEnter={() => handlePopupVisible(true)}
                    onMouseLeave={() => handlePopupVisible(false)}
                >
                    <img
                        className="popup-img"
                        alt='image'
                        src='https://cdn.discordapp.com/attachments/578931223775281162/644181902215086094/default_geocode-1x.png'
                    />
                    <div className="popup-content">
                        <span className="popup-content-title">{name}</span>
                        {description && <span className="popup-content-subtitle">{description}</span>}
                    </div>
                </div>
            </Popup>
        </Marker>
            
    );
};

export default DynamicMarker;

如果地图缩放低于阈值,上面的代码会从标记解除绑定弹出窗口,并在缩放高于阈值时绑定它们。我还为标记组件上的onMouseOveronMouseOut 事件实现了事件处理程序,以在用户悬停标记图标时打开我的弹出窗口,并且只有当光标没有悬停在弹出窗口或标记图标上时才会关闭弹出窗口.

当我放大或缩小显示大约 2k 标记时,地图会冻结大约 5-10 秒,并更新 react-leaflet 导出的 Map 组件内的所有组件。

【问题讨论】:

    标签: leaflet react-leaflet


    【解决方案1】:

    在通过react-leaflet-markercluster 使用标记聚类进行测试后,我注意到性能问题仍然存在。我尝试注释掉作为子组件传递给标记组件的Popup 组件,并且我遇到的滞后问题已经消失。

    考虑到这一点,我意识到我的瓶颈实际上是在 DOM 中渲染 2k 弹出窗口,即使它们是不可见的。所以,经过反复试验,我找到了一个解决方案:状态。

    我添加了一个名为shouldDrawPopup 的布尔状态,默认值为false,并且只在handlePopupVisible 函数内更改了它的值。只有在以下情况下,此布尔状态的值才会改变:

    • 地图缩放超过阈值;和
    • 弹窗未打开

    然后我将组件的render 函数更改为仅在shouldDrawPopup 状态为真时才包含弹出窗口:

        return (
            {shouldDrawPopup && (
                <Marker
                    icon={divIcon}
                    position={coords}
                    onmouseover={() => handlePopupVisible(true)}
                    onmouseout={() => handlePopupVisible(false)}
                    ref={markerRef}
                >
                    <Popup className="custom-popup-content" ref={popupRef} closeButton={false}>
                        <div
                            onMouseEnter={() => handlePopupVisible(true)}
                            onMouseLeave={() => handlePopupVisible(false)}
                        >
                            <img
                                className="popup-img"
                                alt='image'
                                src='https://cdn.discordapp.com/attachments/578931223775281162/644181902215086094/default_geocode-1x.png'
                            />
                            <div className="popup-content">
                                <span className="popup-content-title">{name}</span>
                                {description && <span className="popup-content-subtitle">{description}</span>}
                            </div>
                        </div>
                    </Popup>
                </Marker>
            )}     
        );
    

    如果有人对此问题有其他解决方案或任何反馈,请随时分享!

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-09-12
      • 1970-01-01
      • 2020-06-22
      • 2019-04-01
      • 1970-01-01
      • 2021-02-14
      • 2019-01-11
      • 1970-01-01
      相关资源
      最近更新 更多