我有一个带有 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;
如果地图缩放低于阈值,上面的代码会解除弹出窗口与标记的绑定,并在缩放高于阈值时绑定它们。我还在标记组件上实现了
onMouseOver
和 onMouseOut
事件的事件处理程序,以便在用户将鼠标悬停在标记图标上时打开弹出窗口,并且仅当光标未悬停在弹出窗口或标记图标上时才会关闭弹出窗口.
当我放大或缩小并显示大约 2k 标记时,地图会冻结大约 5-10 秒,并更新由
Map
导出的 react-leaflet
组件内的所有组件。
通过
react-leaflet-markercluster
对标记聚类进行测试后,我注意到性能问题仍然存在。我尝试注释掉作为子组件传递给标记组件的 Popup
组件,并且我遇到的滞后问题消失了。
考虑到这一点,我意识到我的瓶颈实际上是在 DOM 中渲染 2k 弹出窗口,即使它们是不可见的。因此,经过一番尝试和错误,我找到了一个解决方案:states。
我添加了一个名为
shouldDrawPopup
的布尔状态,默认值为 false
,并且仅在 handlePopupVisible
函数内更改了其值。仅当满足以下条件时,此布尔状态的值才会更改:
然后我更改了组件的
render
函数,仅当 shouldDrawPopup
状态为 true 时才包含弹出窗口:
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>
)}
);
如果有人对此问题有其他解决方案或任何反馈,请随时分享!
这对我有用:
export const AutoOpenPopup = ({ children }) => {
const popupRef = useRef(null);
useEffect(() => {
if (popupRef.current) {
const marker = popupRef.current._source;
if (marker) {
marker.openPopup();
}
}
}, []);
return <Popup ref={popupRef}>{children}</Popup>;
};
我将它与这样的标记一起使用:
const handleMarkerClick = e => {
setIsClicked(true);
};
<Marker eventHandlers={{
click: handleMarkerClick,
}}
>
{isClicked && <AutoOpenPopup> Popup's content </AutoOpenPopup }
</Marker>