# Listing Map Performance Analysis **Date:** 2026-04-24 **Component:** `apps/web/components/map/listing-map.tsx` **Issue:** [GOO-132](/GOO/issues/GOO-132) --- ## Baseline Regressions Identified ### 1. DOM Marker Thrash on `selectedListingId` Change (Critical) **Problem:** The marker `useEffect` depended on both `markers` **and** `selectedListingId`. Every time a user hovered/selected a listing, all 200+ markers were: - `m.remove()` called on each `mapboxgl.Marker` - New `document.createElement('button')` for every marker - New `mapboxgl.Marker()` and `.addTo(map)` for every marker - `fitBounds` re-fired, causing unwanted camera jump At 200 listings this is ~200 DOM node destructions + 200 DOM creations + 200 Mapbox GL marker registrations per hover event. **Fix:** Migrated from DOM markers to a Mapbox GL `GeoJSON` source with `cluster: true`. Selection state is now expressed as a `selected: 0|1` property on each GeoJSON feature, filtered into a separate symbol layer. Updating selection only calls `source.setData()` once — zero DOM allocation. --- ### 2. No Marker Clustering (Critical for 200+ listings) **Problem:** Each listing was rendered as an independent `mapboxgl.Marker` (a full DOM element). At 200+ markers: - Overlapping markers made the map unusable - Each marker participates in Mapbox's internal DOM layout/hit-test on every pan frame - Mobile (Android mid-range) drops below 60fps at ~80+ DOM markers **Fix:** Enabled Mapbox built-in GeoJSON source clustering (`cluster: true`, `clusterRadius: 50`, `clusterMaxZoom: 14`). Clusters render as WebGL `circle` layers — GPU-composited, zero per-frame DOM cost. At any viewport, the engine renders at most O(viewport tiles) features, not O(all listings). **Decision — supercluster vs Mapbox built-in:** Chose **Mapbox built-in clustering** because: - No extra dependency - Cluster expansion zoom is available via `getClusterExpansionZoom()` - Sufficient for listing counts up to ~5 000 (beyond that, supercluster's worker thread wins) - Avoids data duplication between a JS-side supercluster index and the Mapbox source Revisit if listing count exceeds 5 000 per search result set. --- ### 3. `fitBounds` Triggered on Every Selection Change **Problem:** `fitBounds` was called inside the same effect that fired on `selectedListingId` changes, so selecting any listing caused a camera jump. Jarring on mobile. **Fix:** `fitBounds` now only runs in the `geojson`-dependent effect (fires on listings array identity change). The selection effect updates GeoJSON data without touching the camera. --- ### 4. `onMarkerClick` Closure Stale Reference **Problem:** The click listener inside `useEffect` captured `onMarkerClick` at mount time. If the parent re-rendered with a new callback, the stale version was called. **Fix:** `onMarkerClickRef` pattern — ref is updated on every render, click handler reads via ref. --- ## Performance Target Assessment | Metric | Before | After (estimate) | |--------|--------|-----------------| | Marker DOM nodes at 200 listings | 200 `