I have a React application with the React Leaflet library and I'm displaying a marker for each building in the map, in a small town. I have about 5k markers in total and a filter to display only the markers I want.
However, I noticed that I was having a huge performance hit with the code below. I've looked at some alternatives such as PixiOverlay and marker clustering, but the former is quite complicated to migrate the current code base to and the latter doesn't solve my problems at all.
My current code:
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;
The code above unbinds popups from markers if the map zoom is below a threshold, and binds them when the zoom is above the threshold. I also implemented event handlers to onMouseOver
and onMouseOut
events on the marker component to open my popup when the user hovers the marker icon and it will only close the popup if the cursor isn't hovering over the popup or the marker icon.
When I zoom in or out with about 2k markers being displayed, the map freezes for about 5-10 seconds and updates all of the components inside the Map
component exported by react-leaflet
.