diff --git a/src/components/SpotMap.tsx b/src/components/SpotMap.tsx index b7735ac..a1699d5 100644 --- a/src/components/SpotMap.tsx +++ b/src/components/SpotMap.tsx @@ -794,6 +794,9 @@ export function SpotMap({ // MarkerClusterGroup for spot/shop/gear/tour markers // eslint-disable-next-line @typescript-eslint/no-explicit-any const clusterGroupRef = useRef(null); + // Signature of the marker set currently on the map — lets us skip a full + // rebuild when the parent hands us new arrays with identical content. + const markerSigRef = useRef(''); const onMapMoveRef = useRef(onMapMove); const navigate = useNavigate(); @@ -1052,6 +1055,38 @@ export function SpotMap({ // Wait for map to be fully loaded before adding markers if (!map.getContainer()) return; + // Skip the whole rebuild when nothing that affects the rendered markers has + // actually changed. Every spot/listing/tour/event/crew is an addressable + // event, so a new event id means an edit — ids + weather colours are enough + // to detect real changes. Without this, the unrelated identity churn the + // parent hands us (trust scores, comment/zap totals and re-sorts all produce + // fresh arrays) tore down and rebuilt every Leaflet marker several times per + // load, which is what made the pins feel slow. + const markerSig = [ + `${showSpots}|${showShops}|${showGear}|${showTours}|${showEvents}|${showCrews}|${weatherEnabled}`, + locationGroups + .map((group) => { + const first = group.spots[0]; + const weather = + weatherEnabled && weatherData && first?.geohash + ? weatherData.get(first.geohash) + : undefined; + return [ + group.geohash, + group.spots.map((s) => s.event.id).join('.'), + group.marketplaceItems.map((i) => i.event.id).join('.'), + weather?.skateability.color ?? '', + weather?.weather.weatherIcon ?? '', + ].join(':'); + }) + .join('|'), + tours.map((t) => t.id).join('.'), + events.map((e) => e.id).join('.'), + crews.map((c) => c.id).join('.'), + ].join('~~'); + if (markerSig === markerSigRef.current) return; + markerSigRef.current = markerSig; + // Clear existing markers from cluster group and array if (clusterGroupRef.current) { clusterGroupRef.current.clearLayers(); @@ -1059,6 +1094,9 @@ export function SpotMap({ markersRef.current.forEach((marker) => marker.remove()); markersRef.current = []; + // Markers built this pass; added to the cluster group in batches below. + const pending: L.Marker[] = []; + // Ensure we have location data before adding markers if (locationGroups.length === 0) return; @@ -1180,12 +1218,8 @@ export function SpotMap({ }); } - // Add to cluster group (enables auto-clustering at high densities) - if (clusterGroupRef.current) { - clusterGroupRef.current.addLayer(marker); - } else { - marker.addTo(map); - } + // Queue for a batched add to the cluster group (see flush below). + pending.push(marker); markersRef.current.push(marker); }); @@ -1284,12 +1318,8 @@ export function SpotMap({ className: 'custom-popup tour-popup', }); - // Add tour markers to cluster group as well - if (clusterGroupRef.current) { - clusterGroupRef.current.addLayer(tourMarker); - } else { - tourMarker.addTo(map); - } + // Queue for a batched add to the cluster group (see flush below). + pending.push(tourMarker); markersRef.current.push(tourMarker); }); } @@ -1357,11 +1387,8 @@ export function SpotMap({ className: 'custom-popup event-popup', }); - if (clusterGroupRef.current) { - clusterGroupRef.current.addLayer(eventMarker); - } else { - eventMarker.addTo(map); - } + // Queue for a batched add to the cluster group (see flush below). + pending.push(eventMarker); markersRef.current.push(eventMarker); }); } @@ -1460,15 +1487,42 @@ export function SpotMap({ className: 'custom-popup crew-popup', }); - if (clusterGroupRef.current) { - clusterGroupRef.current.addLayer(crewMarker); - } else { - crewMarker.addTo(map); - } + // Queue for a batched add to the cluster group (see flush below). + pending.push(crewMarker); markersRef.current.push(crewMarker); }); } - }, [locationGroups, tours, showTours, events, showEvents, crews, showCrews, onSpotClick, handleViewDetails, weatherEnabled, weatherData, navigate]); + + // Add the queued markers to the cluster group in batches across animation + // frames. `addLayers` is the plugin's bulk API — it clusters the batch and + // refreshes once, instead of the old one-marker-at-a-time `addLayer`, which + // re-clustered the whole set for every single pin. Batching keeps the main + // thread responsive (no freeze on a few hundred pins) and lets nearby pins + // paint first; cancel it if the effect re-runs before it finishes. + const group = clusterGroupRef.current; + if (!group) { + pending.forEach((marker) => marker.addTo(map)); + } else if (pending.length > 0) { + const CHUNK_SIZE = 150; + let offset = 0; + let cancelled = false; + let raf = 0; + const flush = () => { + if (cancelled || !clusterGroupRef.current) return; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (clusterGroupRef.current as any).addLayers(pending.slice(offset, offset + CHUNK_SIZE)); + offset += CHUNK_SIZE; + if (offset < pending.length) { + raf = requestAnimationFrame(flush); + } + }; + flush(); + return () => { + cancelled = true; + if (raf) cancelAnimationFrame(raf); + }; + } + }, [locationGroups, tours, showTours, events, showEvents, crews, showCrews, showSpots, showShops, showGear, onSpotClick, handleViewDetails, weatherEnabled, weatherData, navigate]); // Update BTC Map markers useEffect(() => {