perf(map): batch + de-dupe marker rendering so pins appear faster
The spot/tour/event/crew marker effect rebuilt every Leaflet marker whenever the parent handed it new arrays, and added each marker with a per-marker clusterGroup.addLayer() call — each of which re-clusters the whole set. On a map with a few hundred pins that meant several full teardown/rebuild passes per load, all synchronously on the main thread. - Skip the rebuild when the marker signature (ids + weather colours + layer toggles) is unchanged. Addressable events get a new id on every edit, so ids reliably detect real changes; unrelated array churn (trust scores, comment/zap totals, re-sorts) no longer rebuilds the map. - Collect markers and add them with the plugin's bulk addLayers() in requestAnimationFrame batches of 150, cancelling on effect re-run. One cluster refresh per batch, pins paint progressively, UI stays responsive.
This commit is contained in:
parent
eb209452fc
commit
563d91504d
1 changed files with 77 additions and 23 deletions
|
|
@ -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<any>(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<string>('');
|
||||
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(() => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue