From 8cb4c88bef6688dd9fe7364545878b5b181f7257 Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Tue, 25 Nov 2025 07:11:25 -0600 Subject: [PATCH] perf: add spatial filtering to skip distant regions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CRITICAL PERFORMANCE FIX: Skip DOM queries for regions far from cursor using pre-computed region centers and distance check. Problem: - Still iterating ALL ~200 regions on every mouse move - Calling getBoundingClientRect() on each one (expensive DOM query) - Even with early returns, doing 200 DOM queries per frame Solution: - Use pre-computed region.center (already in map data) - Convert center to screen coords using matrixTransform() - Calculate distance from cursor to region center - Skip regions > 150px away BEFORE touching DOM - Only query DOM for nearby regions (~5-10 typically) Performance improvement: - Before: 200 DOM queries per mouse move - After: ~5-10 DOM queries per mouse move (only nearby regions) - 95% reduction in DOM queries Distance threshold: - Detection box is 50px - Using 150px threshold (3× detection box size) - Generous to avoid false negatives for large regions - Distance check uses squared distance (no sqrt needed) This is the proper "broad phase" optimization before expensive checks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../hooks/useRegionDetection.ts | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/apps/web/src/arcade-games/know-your-world/hooks/useRegionDetection.ts b/apps/web/src/arcade-games/know-your-world/hooks/useRegionDetection.ts index 3f2b3521..c7c00906 100644 --- a/apps/web/src/arcade-games/know-your-world/hooks/useRegionDetection.ts +++ b/apps/web/src/arcade-games/know-your-world/hooks/useRegionDetection.ts @@ -173,7 +173,46 @@ export function useRegionDetection(options: UseRegionDetectionOptions): UseRegio let totalRegionArea = 0 let detectedSmallestSize = Infinity + // Get SVG transformation for converting region centers to screen coords + const screenCTM = svgElement.getScreenCTM() + if (!screenCTM) { + return { + detectedRegions: [], + regionUnderCursor: null, + regionUnderCursorArea: 0, + regionsInBox: 0, + hasSmallRegion: false, + detectedSmallestSize: Infinity, + totalRegionArea: 0, + } + } + + const viewBoxParts = mapData.viewBox.split(' ').map(Number) + const viewBoxX = viewBoxParts[0] || 0 + const viewBoxY = viewBoxParts[1] || 0 + mapData.regions.forEach((region) => { + // PERFORMANCE: Quick distance check using pre-computed center + // This avoids expensive DOM queries for regions far from cursor + // Region center is in SVG coordinates, convert to screen coords + const svgCenter = svgElement.createSVGPoint() + svgCenter.x = region.center[0] + svgCenter.y = region.center[1] + const screenCenter = svgCenter.matrixTransform(screenCTM) + + // Calculate rough distance from cursor to region center + const dx = screenCenter.x - cursorClientX + const dy = screenCenter.y - cursorClientY + const distanceSquared = dx * dx + dy * dy + + // Skip regions whose centers are far from the detection box + // Use generous threshold to avoid false negatives (detection box is 50px, so check 150px) + const MAX_DISTANCE = 150 + if (distanceSquared > MAX_DISTANCE * MAX_DISTANCE) { + return // Region is definitely too far away + } + + // Region is close enough - now do proper DOM-based checks const regionPath = svgElement.querySelector(`path[data-region-id="${region.id}"]`) if (!regionPath || !(regionPath instanceof SVGGeometryElement)) return @@ -183,7 +222,7 @@ export function useRegionDetection(options: UseRegionDetectionOptions): UseRegio // Sample multiple points within the detection box to check for intersection // This prevents false positives from irregularly shaped regions - // First quick check: does bounding box even overlap? (fast rejection) + // Second check: does bounding box overlap? (fast rejection) const regionLeft = pathRect.left const regionRight = pathRect.right const regionTop = pathRect.top