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
|
// MarkerClusterGroup for spot/shop/gear/tour markers
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const clusterGroupRef = useRef<any>(null);
|
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 onMapMoveRef = useRef(onMapMove);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
|
@ -1052,6 +1055,38 @@ export function SpotMap({
|
||||||
// Wait for map to be fully loaded before adding markers
|
// Wait for map to be fully loaded before adding markers
|
||||||
if (!map.getContainer()) return;
|
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
|
// Clear existing markers from cluster group and array
|
||||||
if (clusterGroupRef.current) {
|
if (clusterGroupRef.current) {
|
||||||
clusterGroupRef.current.clearLayers();
|
clusterGroupRef.current.clearLayers();
|
||||||
|
|
@ -1059,6 +1094,9 @@ export function SpotMap({
|
||||||
markersRef.current.forEach((marker) => marker.remove());
|
markersRef.current.forEach((marker) => marker.remove());
|
||||||
markersRef.current = [];
|
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
|
// Ensure we have location data before adding markers
|
||||||
if (locationGroups.length === 0) return;
|
if (locationGroups.length === 0) return;
|
||||||
|
|
||||||
|
|
@ -1180,12 +1218,8 @@ export function SpotMap({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to cluster group (enables auto-clustering at high densities)
|
// Queue for a batched add to the cluster group (see flush below).
|
||||||
if (clusterGroupRef.current) {
|
pending.push(marker);
|
||||||
clusterGroupRef.current.addLayer(marker);
|
|
||||||
} else {
|
|
||||||
marker.addTo(map);
|
|
||||||
}
|
|
||||||
markersRef.current.push(marker);
|
markersRef.current.push(marker);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1284,12 +1318,8 @@ export function SpotMap({
|
||||||
className: 'custom-popup tour-popup',
|
className: 'custom-popup tour-popup',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add tour markers to cluster group as well
|
// Queue for a batched add to the cluster group (see flush below).
|
||||||
if (clusterGroupRef.current) {
|
pending.push(tourMarker);
|
||||||
clusterGroupRef.current.addLayer(tourMarker);
|
|
||||||
} else {
|
|
||||||
tourMarker.addTo(map);
|
|
||||||
}
|
|
||||||
markersRef.current.push(tourMarker);
|
markersRef.current.push(tourMarker);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -1357,11 +1387,8 @@ export function SpotMap({
|
||||||
className: 'custom-popup event-popup',
|
className: 'custom-popup event-popup',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (clusterGroupRef.current) {
|
// Queue for a batched add to the cluster group (see flush below).
|
||||||
clusterGroupRef.current.addLayer(eventMarker);
|
pending.push(eventMarker);
|
||||||
} else {
|
|
||||||
eventMarker.addTo(map);
|
|
||||||
}
|
|
||||||
markersRef.current.push(eventMarker);
|
markersRef.current.push(eventMarker);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -1460,15 +1487,42 @@ export function SpotMap({
|
||||||
className: 'custom-popup crew-popup',
|
className: 'custom-popup crew-popup',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (clusterGroupRef.current) {
|
// Queue for a batched add to the cluster group (see flush below).
|
||||||
clusterGroupRef.current.addLayer(crewMarker);
|
pending.push(crewMarker);
|
||||||
} else {
|
|
||||||
crewMarker.addTo(map);
|
|
||||||
}
|
|
||||||
markersRef.current.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
|
// Update BTC Map markers
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue