perf: major trip planner performance overhaul (#218)
Store & re-render optimization: - TripPlannerPage uses selective Zustand selectors instead of full store - placesSlice only updates affected days on place update/delete - Route calculation only reacts to selected day's assignments - DayPlanSidebar uses stable action refs instead of full store Map marker performance: - Shared photoService for PlaceAvatar and MapView (single cache, no duplicate requests) - Client-side base64 thumbnail generation via canvas (CORS-safe for Wikimedia) - Map markers use base64 data URL <img> tags for smooth zoom (no external image decode) - Sidebar uses same base64 thumbnails with IntersectionObserver for visible-first loading - Icon cache prevents duplicate L.divIcon creation - MarkerClusterGroup with animate:false and optimized chunk settings - Photo fetch deduplication and batched state updates Server optimizations: - Wikimedia image size reduced to 400px (from 600px) - Photo cache: 5min TTL for errors (was 12h), prevents stale 404 caching - Removed unused image-proxy endpoint UX improvements: - Splash screen with plane animation during initial photo preload - Markdown rendering in DayPlanSidebar place descriptions - Missing i18n keys added, all 12 languages synced to 1376 keys
This commit is contained in:
@@ -154,7 +154,7 @@ async function fetchWikimediaPhoto(lat: number, lng: number, name?: string): Pro
|
||||
ggslimit: '5',
|
||||
prop: 'imageinfo',
|
||||
iiprop: 'url|extmetadata|mime',
|
||||
iiurlwidth: '600',
|
||||
iiurlwidth: '400',
|
||||
});
|
||||
try {
|
||||
const res = await fetch(`https://commons.wikimedia.org/w/api.php?${params}`, { headers: { 'User-Agent': UA } });
|
||||
@@ -380,11 +380,14 @@ router.get('/place-photo/:placeId', authenticate, async (req: Request, res: Resp
|
||||
const { placeId } = req.params;
|
||||
|
||||
const cached = photoCache.get(placeId);
|
||||
if (cached && Date.now() - cached.fetchedAt < PHOTO_TTL) {
|
||||
if (cached.error) {
|
||||
return res.status(404).json({ error: `(Cache) No photo available` });
|
||||
const ERROR_TTL = 5 * 60 * 1000; // 5 min for errors
|
||||
if (cached) {
|
||||
const ttl = cached.error ? ERROR_TTL : PHOTO_TTL;
|
||||
if (Date.now() - cached.fetchedAt < ttl) {
|
||||
if (cached.error) return res.status(404).json({ error: `(Cache) No photo available` });
|
||||
return res.json({ photoUrl: cached.photoUrl, attribution: cached.attribution });
|
||||
}
|
||||
return res.json({ photoUrl: cached.photoUrl, attribution: cached.attribution });
|
||||
photoCache.delete(placeId);
|
||||
}
|
||||
|
||||
// Wikimedia Commons fallback for OSM places (using lat/lng query params)
|
||||
@@ -436,7 +439,7 @@ router.get('/place-photo/:placeId', authenticate, async (req: Request, res: Resp
|
||||
const attribution = photo.authorAttributions?.[0]?.displayName || null;
|
||||
|
||||
const mediaRes = await fetch(
|
||||
`https://places.googleapis.com/v1/${photoName}/media?maxHeightPx=600&skipHttpRedirect=true`,
|
||||
`https://places.googleapis.com/v1/${photoName}/media?maxHeightPx=400&skipHttpRedirect=true`,
|
||||
{ headers: { 'X-Goog-Api-Key': apiKey } }
|
||||
);
|
||||
const mediaData = await mediaRes.json() as { photoUri?: string };
|
||||
|
||||
Reference in New Issue
Block a user