From 8ef6868881508ee3b3487f2d4be6194629f60712 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sun, 22 Feb 2026 13:29:54 +0000 Subject: [PATCH] Eliminate frontend POI waterfall for faster initial load Listing stream fires immediately on auth without waiting for POI fetch. POI distances are not needed for initial rendering and are only computed when user selects POI metric or sets travel filters. This saves ~200-500ms on initial load and keeps the stream on the cached Redis path. --- frontend/src/App.tsx | 38 +++++++++++++++++++++-- frontend/src/services/streamingService.ts | 5 +-- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bf2e353..5c35cb6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,7 +17,7 @@ import { Sheet, SheetContent, SheetTrigger } from './components/ui/sheet'; import { Button } from './components/ui/button'; import { Filter, Heart } from 'lucide-react'; import type { GeoJSONFeatureCollection, PropertyProperties, PropertyFeature, POI, POITravelFilter } from '@/types'; -import { refreshListings, streamListingGeoJSON, fetchUserPOIs, type StreamingProgress } from '@/services'; +import { refreshListings, streamListingGeoJSON, fetchUserPOIs, fetchBulkPOIDistances, type StreamingProgress } from '@/services'; import { setOnUnauthorized } from '@/services/apiClient'; import { clearPasskeyUser } from './auth/passkeyService'; import { poiMetricPropertyName, injectPoiMetricProperty } from '@/utils/poiUtils'; @@ -167,7 +167,7 @@ function App() { try { for await (const batch of streamListingGeoJSON(user, parameters, (progress) => { setStreamingProgress(progress); - }, { includePoiDistances: userPOIs.length > 0, signal: controller.signal })) { + }, { signal: controller.signal })) { // Deduplicate features by URL const uniqueBatch = batch.filter((feature) => { const url = feature.properties?.url; @@ -200,7 +200,39 @@ function App() { setStreamingProgress(null); } } - }, [user, userPOIs]); + }, [user]); + + // Merge POI distances into listing data after both are available + useEffect(() => { + if (!user || !listingData || userPOIs.length === 0 || !queryParameters) return; + + let cancelled = false; + fetchBulkPOIDistances(user, (queryParameters.listing_type as 'RENT' | 'BUY') ?? 'RENT') + .then((distanceLookup) => { + if (cancelled) return; + const updatedFeatures = accumulatedFeaturesRef.current.map(feature => { + const id = feature.properties?.id; + if (id && distanceLookup[id]) { + return { + ...feature, + properties: { + ...feature.properties, + poi_distances: distanceLookup[id], + }, + }; + } + return feature; + }); + accumulatedFeaturesRef.current = updatedFeatures; + setListingData({ + type: 'FeatureCollection', + features: [...updatedFeatures], + }); + }) + .catch(() => {}); // POI distances are best-effort + + return () => { cancelled = true; }; + }, [user, listingData?.features.length, userPOIs.length, queryParameters]); // Compute processed listing data: inject synthetic POI metric property & apply max travel filter const processedListingData = useMemo(() => { diff --git a/frontend/src/services/streamingService.ts b/frontend/src/services/streamingService.ts index ba681a3..730ec78 100644 --- a/frontend/src/services/streamingService.ts +++ b/frontend/src/services/streamingService.ts @@ -69,12 +69,9 @@ export async function* streamListingGeoJSON( user: AuthUser, parameters: ParameterValues, onProgress?: (progress: StreamingProgress) => void, - options?: { includePoiDistances?: boolean; signal?: AbortSignal }, + options?: { signal?: AbortSignal }, ): AsyncGenerator { const params = buildListingParams(parameters); - if (options?.includePoiDistances) { - params.include_poi_distances = 'true'; - } const queryString = buildQueryString(params); const url = queryString ? `${API_ENDPOINTS.LISTING_GEOJSON_STREAM}?${queryString}`