From bd3e4bee4e9c549fa46dd76bc870cc81a0affb5b Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Thu, 18 Jun 2026 11:32:24 +0700 Subject: [PATCH] perf: lazy loading, faster timeouts, better caching, chunk splitting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AppRouter: lazy load all 40+ pages (only Index is eager) → initial JS bundle no longer includes code for pages never visited - MapPage: lazy load SpotMap/Leaflet (~150KB) behind Suspense → Leaflet only downloads when user navigates to /map - useSkateSpots/useShops/useMarketplace/useTours: → timeout: 30s → 8s (fail fast, show error sooner) → staleTime: 30s → 5min (trust cache, fewer relay round-trips) → gcTime: 5min → 30min (keep data in memory longer) → refetchOnMount: 'always' → true (use cache when data is fresh) - Remove console.log/warn/debug spam from production hooks - vite.config: manual chunk splitting for leaflet, nostr-tools, bitcoin libs, radix-ui, react-router — each chunk cached independently by browser, only re-downloaded when that library updates - vite.config: esbuild drops console.log/warn/debug in prod build Co-authored-by: shakespeare.diy --- src/AppRouter.tsx | 206 ++++++++++++++++++++---------------- src/hooks/useMarketplace.ts | 32 +++--- src/hooks/useShops.ts | 37 ++----- src/hooks/useSkateSpots.ts | 57 +++++----- src/hooks/useTours.ts | 48 ++++----- src/pages/MapPage.tsx | 53 ++++++---- vite.config.ts | 51 ++++++++- 7 files changed, 258 insertions(+), 226 deletions(-) diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index 9f6f53e..b63bfdf 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -1,106 +1,124 @@ +import { lazy, Suspense } from "react"; import { BrowserRouter, Route, Routes } from "react-router-dom"; import { ScrollToTop } from "./components/ScrollToTop"; +// Eagerly load only the most-visited pages import Index from "./pages/Index"; -import MapPage from "./pages/MapPage"; -import EventsPage from "./pages/EventsPage"; -import FoodPage from "./pages/FoodPage"; -import WalletPage from "./pages/WalletPage"; -import SettingsPage from "./pages/SettingsPage"; -import CreateSpotPage from "./pages/CreateSpotPage"; -import CreateShopPage from "./pages/CreateShopPage"; -import CreateEventPage from "./pages/CreateEventPage"; -import CreateListingPage from "./pages/CreateListingPage"; -import CreateTourPage from "./pages/CreateTourPage"; -import EditSpotPage from "./pages/EditSpotPage"; -import EditShopPage from "./pages/EditShopPage"; -import EditEventPage from "./pages/EditEventPage"; -import GearSwapPage from "./pages/GearSwapPage"; -import SpotDetailPage from "./pages/SpotDetailPage"; -import ShopDetailPage from "./pages/ShopDetailPage"; -import EventDetailPage from "./pages/EventDetailPage"; -import OnboardingPage from "./pages/OnboardingPage"; -import PyramidPage from "./pages/PyramidPage"; -import LabPage from "./pages/LabPage"; -import BecomeCoachPage from "./pages/BecomeCoachPage"; -import BecomeFilmerPage from "./pages/BecomeFilmerPage"; -import BecomeEditorPage from "./pages/BecomeEditorPage"; -import CoachDetailPage from "./pages/CoachDetailPage"; -import TourDetailPage from "./pages/TourDetailPage"; -import AboutPage from "./pages/AboutPage"; -import FAQPage from "./pages/FAQPage"; -import CommunityStatsPage from "./pages/CommunityStatsPage"; -import SpotCollectionsPage from "./pages/SpotCollectionsPage"; -import CollectionDetailPage from "./pages/CollectionDetailPage"; -import DirectMessagesPage from "./pages/DirectMessagesPage"; -import ConversationPage from "./pages/ConversationPage"; -import BountyLeaderboardPage from "./pages/BountyLeaderboardPage"; -import SignersPage from "./pages/SignersPage"; -import StashPage from "./pages/StashPage"; -import { NIP19Page } from "./pages/NIP19Page"; -import LicensePage from "./pages/LicensePage"; -import SupportersPage from "./pages/SupportersPage"; -import RiderCardPage from "./pages/RiderCardPage"; -import AmberCallbackPage from "./pages/AmberCallbackPage"; -import NotFound from "./pages/NotFound"; + +// Lazy load everything else — each page becomes its own JS chunk +const MapPage = lazy(() => import("./pages/MapPage")); +const EventsPage = lazy(() => import("./pages/EventsPage")); +const LabPage = lazy(() => import("./pages/LabPage")); +const WalletPage = lazy(() => import("./pages/WalletPage")); +const SettingsPage = lazy(() => import("./pages/SettingsPage")); +const CreateSpotPage = lazy(() => import("./pages/CreateSpotPage")); +const CreateShopPage = lazy(() => import("./pages/CreateShopPage")); +const CreateEventPage = lazy(() => import("./pages/CreateEventPage")); +const CreateListingPage = lazy(() => import("./pages/CreateListingPage")); +const CreateTourPage = lazy(() => import("./pages/CreateTourPage")); +const EditSpotPage = lazy(() => import("./pages/EditSpotPage")); +const EditShopPage = lazy(() => import("./pages/EditShopPage")); +const EditEventPage = lazy(() => import("./pages/EditEventPage")); +const GearSwapPage = lazy(() => import("./pages/GearSwapPage")); +const SpotDetailPage = lazy(() => import("./pages/SpotDetailPage")); +const ShopDetailPage = lazy(() => import("./pages/ShopDetailPage")); +const EventDetailPage = lazy(() => import("./pages/EventDetailPage")); +const OnboardingPage = lazy(() => import("./pages/OnboardingPage")); +const PyramidPage = lazy(() => import("./pages/PyramidPage")); +const BecomeCoachPage = lazy(() => import("./pages/BecomeCoachPage")); +const BecomeFilmerPage = lazy(() => import("./pages/BecomeFilmerPage")); +const BecomeEditorPage = lazy(() => import("./pages/BecomeEditorPage")); +const CoachDetailPage = lazy(() => import("./pages/CoachDetailPage")); +const TourDetailPage = lazy(() => import("./pages/TourDetailPage")); +const AboutPage = lazy(() => import("./pages/AboutPage")); +const FAQPage = lazy(() => import("./pages/FAQPage")); +const FoodPage = lazy(() => import("./pages/FoodPage")); +const CommunityStatsPage = lazy(() => import("./pages/CommunityStatsPage")); +const SpotCollectionsPage = lazy(() => import("./pages/SpotCollectionsPage")); +const CollectionDetailPage = lazy(() => import("./pages/CollectionDetailPage")); +const DirectMessagesPage = lazy(() => import("./pages/DirectMessagesPage")); +const ConversationPage = lazy(() => import("./pages/ConversationPage")); +const BountyLeaderboardPage = lazy(() => import("./pages/BountyLeaderboardPage")); +const SignersPage = lazy(() => import("./pages/SignersPage")); +const StashPage = lazy(() => import("./pages/StashPage")); +const NIP19Page = lazy(() => import("./pages/NIP19Page").then(m => ({ default: m.NIP19Page }))); +const LicensePage = lazy(() => import("./pages/LicensePage")); +const SupportersPage = lazy(() => import("./pages/SupportersPage")); +const RiderCardPage = lazy(() => import("./pages/RiderCardPage")); +const AmberCallbackPage = lazy(() => import("./pages/AmberCallbackPage")); +const MarketplacePage = lazy(() => import("./pages/MarketplacePage")); +const NotFound = lazy(() => import("./pages/NotFound")); + +// Minimal inline fallback — no extra component needed +function PageLoader() { + return ( +
+
+
+ ); +} export function AppRouter() { return ( - - } /> - } /> - } /> - } /> - {/* Legacy route redirect */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - {/* Bounty System */} - } /> - {/* Security & Signers */} - } /> - {/* Stash - Private Inventory */} - } /> - {/* Info pages */} - } /> - } /> - } /> - {/* Amber NIP-55 callback — Amber redirects here after signing/login */} - } /> - {/* NIP-19 route for npub1, note1, naddr1, nevent1, nprofile1 */} - } /> - {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} - } /> - + }> + + } /> + } /> + } /> + } /> + {/* Legacy route redirect */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {/* Bounty System */} + } /> + {/* Security & Signers */} + } /> + {/* Stash - Private Inventory */} + } /> + {/* Info pages */} + } /> + } /> + } /> + {/* Amber NIP-55 callback — Amber redirects here after signing/login */} + } /> + {/* Marketplace */} + } /> + {/* NIP-19 route for npub1, note1, naddr1, nevent1, nprofile1 */} + } /> + {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} + } /> + + ); } diff --git a/src/hooks/useMarketplace.ts b/src/hooks/useMarketplace.ts index 21d248c..9822b48 100644 --- a/src/hooks/useMarketplace.ts +++ b/src/hooks/useMarketplace.ts @@ -2,15 +2,17 @@ import { useQuery } from '@tanstack/react-query'; import { MARKETPLACE_ITEM_KIND, parseMarketplaceItem, type MarketplaceItem } from '@/lib/types'; import { useSkateRelays } from '@/hooks/useSkateRelays'; +const STALE_TIME = 5 * 60 * 1000; // 5 min +const GC_TIME = 30 * 60 * 1000; // 30 min +const TIMEOUT = 8000; + export function useMarketplaceItems(limit = 100) { const { querySkateContent, getQueryKeySuffix } = useSkateRelays(); return useQuery({ queryKey: ['marketplace-items', limit, getQueryKeySuffix()], queryFn: async () => { - const signal = AbortSignal.timeout(10000); - - console.log('[useMarketplaceItems] Querying for marketplace items...'); + const signal = AbortSignal.timeout(TIMEOUT); const events = await querySkateContent( [ @@ -23,20 +25,17 @@ export function useMarketplaceItems(limit = 100) { { signal } ); - console.log(`[useMarketplaceItems] Received ${events.length} events`); - const items: MarketplaceItem[] = []; for (const event of events) { const item = parseMarketplaceItem(event); if (item) items.push(item); } - console.log(`[useMarketplaceItems] Parsed items: ${items.length}`); - return items.sort((a, b) => b.createdAt - a.createdAt); }, - staleTime: 30000, - refetchOnMount: 'always', + staleTime: STALE_TIME, + gcTime: GC_TIME, + refetchOnMount: true, }); } @@ -46,13 +45,10 @@ export function useMarketplaceAtSpot(spotPubkey: string, spotIdentifier: string, return useQuery({ queryKey: ['marketplace-at-spot', spotPubkey, spotIdentifier, getQueryKeySuffix()], queryFn: async () => { - const signal = AbortSignal.timeout(10000); + const signal = AbortSignal.timeout(TIMEOUT); - // Build spot reference (naddr format without encoding) const spotRef = `30818:${spotPubkey}:${spotIdentifier}`; - console.log(`[useMarketplaceAtSpot] Querying for items at spot: ${spotRef}`); - const events = await querySkateContent( [ { @@ -65,23 +61,19 @@ export function useMarketplaceAtSpot(spotPubkey: string, spotIdentifier: string, { signal } ); - console.log(`[useMarketplaceAtSpot] Received ${events.length} events`); - const items: MarketplaceItem[] = []; for (const event of events) { const item = parseMarketplaceItem(event); - // Only show items that are active or don't have a status if (item && (!item.status || item.status === 'active')) { items.push(item); } } - console.log(`[useMarketplaceAtSpot] Parsed active items: ${items.length}`); - return items.sort((a, b) => b.createdAt - a.createdAt); }, enabled: !!spotPubkey && !!spotIdentifier, - staleTime: 30000, - refetchOnMount: 'always', + staleTime: STALE_TIME, + gcTime: GC_TIME, + refetchOnMount: true, }); } diff --git a/src/hooks/useShops.ts b/src/hooks/useShops.ts index 8b72f72..05ae996 100644 --- a/src/hooks/useShops.ts +++ b/src/hooks/useShops.ts @@ -10,8 +10,6 @@ export function useShops(limit = 100) { queryFn: async () => { const signal = AbortSignal.timeout(10000); - console.log('[useShops] Querying for shops'); - // Query new shop kind (30017) const newShops = await querySkateContent( [ @@ -23,8 +21,6 @@ export function useShops(limit = 100) { ], { signal } ); - console.log(`[useShops] Received ${newShops.length} new shops (kind 30017)`); - // Query old shops (kind 30818 with 'shop' type) for backward compatibility const oldShops = await querySkateContent( [ @@ -36,7 +32,7 @@ export function useShops(limit = 100) { ], { signal } ); - console.log(`[useShops] Received ${oldShops.length} old shops (kind 30818)`); + const allEvents = [...newShops, ...oldShops]; @@ -72,19 +68,15 @@ export function useShops(limit = 100) { if (shop) { shops.push(shop); - } else { - console.warn('[useShops] Failed to parse event:', event); } } - console.log('[useShops] Parsed shops:', shops.length); - // Sort by created_at descending return shops.sort((a, b) => b.createdAt - a.createdAt); }, - staleTime: 30000, - gcTime: 300000, - refetchOnMount: 'always', + staleTime: 5 * 60 * 1000, + gcTime: 30 * 60 * 1000, + refetchOnMount: true, }); } @@ -96,8 +88,6 @@ export function useShop(identifier: string, pubkey?: string) { queryFn: async () => { const signal = AbortSignal.timeout(10000); - console.log(`[useShop] Querying for shop: ${identifier}`); - // Query new shop format (kind 30017) const newFilter: { kinds: number[]; @@ -112,8 +102,6 @@ export function useShop(identifier: string, pubkey?: string) { if (pubkey) newFilter.authors = [pubkey]; const newShops = await querySkateContent([newFilter], { signal }); - console.log(`[useShop] Received ${newShops.length} new shops`); - // Query old shop format (kind 30818 with 'shop' type) for backward compatibility const oldFilter: { kinds: number[]; @@ -130,14 +118,9 @@ export function useShop(identifier: string, pubkey?: string) { if (pubkey) oldFilter.authors = [pubkey]; const oldShops = await querySkateContent([oldFilter], { signal }); - console.log(`[useShop] Received ${oldShops.length} old shops`); - const allEvents = [...newShops, ...oldShops]; - if (allEvents.length === 0) { - console.warn(`[useShop] No events found for identifier: ${identifier}`); - return null; - } + if (allEvents.length === 0) return null; // If multiple events, take the most recent const mostRecent = allEvents.reduce((latest, current) => @@ -174,8 +157,9 @@ export function useShop(identifier: string, pubkey?: string) { return shop; }, enabled: !!identifier, - staleTime: 30000, - refetchOnMount: 'always', + staleTime: 5 * 60 * 1000, + gcTime: 30 * 60 * 1000, + refetchOnMount: true, }); } @@ -208,7 +192,8 @@ export function useUserShops(pubkey: string) { return shops.sort((a, b) => b.createdAt - a.createdAt); }, enabled: !!pubkey, - staleTime: 30000, - refetchOnMount: 'always', + staleTime: 5 * 60 * 1000, + gcTime: 30 * 60 * 1000, + refetchOnMount: true, }); } diff --git a/src/hooks/useSkateSpots.ts b/src/hooks/useSkateSpots.ts index cf6f22f..e3998a1 100644 --- a/src/hooks/useSkateSpots.ts +++ b/src/hooks/useSkateSpots.ts @@ -3,6 +3,11 @@ import { SKATE_SPOT_KIND, parseSkateSpot, type SkateSpot } from '@/lib/types'; import { useSkateRelays } from '@/hooks/useSkateRelays'; import { cacheSpots, getAllCachedSpots, isOffline } from '@/lib/offlineCache'; +// Spots change infrequently — cache for 5 minutes, keep for 30 minutes +const SPOTS_STALE_TIME = 5 * 60 * 1000; // 5 min +const SPOTS_GC_TIME = 30 * 60 * 1000; // 30 min +const QUERY_TIMEOUT = 8000; // 8s — fail fast, show error sooner + export function useSkateSpots(limit = 100) { const { querySkateContent, getQueryKeySuffix } = useSkateRelays(); @@ -11,12 +16,11 @@ export function useSkateSpots(limit = 100) { queryFn: async () => { // If offline, return cached spots if (isOffline()) { - console.log('[useSkateSpots] Offline, returning cached spots'); const cachedSpots = await getAllCachedSpots(); return cachedSpots.sort((a, b) => b.createdAt - a.createdAt).slice(0, limit); } - const signal = AbortSignal.timeout(30000); + const signal = AbortSignal.timeout(QUERY_TIMEOUT); const events = await querySkateContent( [ @@ -31,32 +35,26 @@ export function useSkateSpots(limit = 100) { const spots: SkateSpot[] = []; const spotsWithEvents: Array<{ event: typeof events[0]; spot: SkateSpot }> = []; - + for (const event of events) { const spot = parseSkateSpot(event); if (spot) { spots.push(spot); spotsWithEvents.push({ event, spot }); - } else { - console.warn('[useSkateSpots] Failed to parse event:', event); } } - console.log('[useSkateSpots] Parsed spots:', spots.length); - // Cache spots in the background for offline use if (spotsWithEvents.length > 0) { - cacheSpots(spotsWithEvents).catch(err => - console.warn('[useSkateSpots] Failed to cache spots:', err) - ); + cacheSpots(spotsWithEvents).catch(() => {}); } - // Sort by created_at descending return spots.sort((a, b) => b.createdAt - a.createdAt); }, - staleTime: 30000, - gcTime: 300000, - refetchOnMount: 'always', + staleTime: SPOTS_STALE_TIME, + gcTime: SPOTS_GC_TIME, + // Don't force a relay round-trip on every navigation — use cache if fresh + refetchOnMount: true, }); } @@ -68,16 +66,12 @@ export function useSkateSpot(identifier: string, pubkey?: string) { queryFn: async () => { // If offline, try to get from cache first if (isOffline() && pubkey) { - console.log('[useSkateSpot] Offline, checking cache'); const { getCachedSpot } = await import('@/lib/offlineCache'); const cached = await getCachedSpot(pubkey, identifier); - if (cached) { - console.log('[useSkateSpot] Found in cache:', identifier); - return cached; - } + if (cached) return cached; } - const signal = AbortSignal.timeout(30000); + const signal = AbortSignal.timeout(QUERY_TIMEOUT); const filter: { kinds: number[]; @@ -96,26 +90,22 @@ export function useSkateSpot(identifier: string, pubkey?: string) { const events = await querySkateContent([filter], { signal }); - if (events.length === 0) { - console.warn(`[useSkateSpot] No events found for identifier: ${identifier}`); - return null; - } + if (events.length === 0) return null; const spot = parseSkateSpot(events[0]); - + // Cache the spot for offline use if (spot) { const { cacheSpot } = await import('@/lib/offlineCache'); - cacheSpot(events[0], spot).catch(err => - console.warn('[useSkateSpot] Failed to cache spot:', err) - ); + cacheSpot(events[0], spot).catch(() => {}); } return spot; }, enabled: !!identifier, - staleTime: 30000, - refetchOnMount: 'always', + staleTime: SPOTS_STALE_TIME, + gcTime: SPOTS_GC_TIME, + refetchOnMount: true, }); } @@ -125,7 +115,7 @@ export function useUserSkateSpots(pubkey: string) { return useQuery({ queryKey: ['user-skate-spots', pubkey, getQueryKeySuffix()], queryFn: async () => { - const signal = AbortSignal.timeout(30000); + const signal = AbortSignal.timeout(QUERY_TIMEOUT); const events = await querySkateContent( [ @@ -148,7 +138,8 @@ export function useUserSkateSpots(pubkey: string) { return spots.sort((a, b) => b.createdAt - a.createdAt); }, enabled: !!pubkey, - staleTime: 30000, - refetchOnMount: 'always', + staleTime: SPOTS_STALE_TIME, + gcTime: SPOTS_GC_TIME, + refetchOnMount: true, }); } diff --git a/src/hooks/useTours.ts b/src/hooks/useTours.ts index ed1307c..0cf6264 100644 --- a/src/hooks/useTours.ts +++ b/src/hooks/useTours.ts @@ -2,15 +2,17 @@ import { useQuery } from '@tanstack/react-query'; import { SKATE_TOUR_KIND, parseSkateTour, type SkateTour } from '@/lib/types'; import { useSkateRelays } from '@/hooks/useSkateRelays'; +const STALE_TIME = 5 * 60 * 1000; // 5 min +const GC_TIME = 30 * 60 * 1000; // 30 min +const TIMEOUT = 8000; + export function useTours(limit = 100) { const { querySkateContent, getQueryKeySuffix } = useSkateRelays(); return useQuery({ queryKey: ['skate-tours', limit, getQueryKeySuffix()], queryFn: async () => { - const signal = AbortSignal.timeout(10000); - - console.log('[useTours] Querying for tours'); + const signal = AbortSignal.timeout(TIMEOUT); const events = await querySkateContent( [ @@ -23,26 +25,17 @@ export function useTours(limit = 100) { { signal } ); - console.log(`[useTours] Received ${events.length} events`); - const tours: SkateTour[] = []; for (const event of events) { const tour = parseSkateTour(event); - if (tour) { - tours.push(tour); - } else { - console.warn('[useTours] Failed to parse event:', event); - } + if (tour) tours.push(tour); } - console.log('[useTours] Parsed tours:', tours.length); - - // Sort by created_at descending return tours.sort((a, b) => b.createdAt - a.createdAt); }, - staleTime: 30000, - gcTime: 300000, - refetchOnMount: 'always', + staleTime: STALE_TIME, + gcTime: GC_TIME, + refetchOnMount: true, }); } @@ -52,7 +45,7 @@ export function useTour(identifier: string, pubkey?: string) { return useQuery({ queryKey: ['skate-tour', identifier, pubkey, getQueryKeySuffix()], queryFn: async () => { - const signal = AbortSignal.timeout(10000); + const signal = AbortSignal.timeout(TIMEOUT); const filter: { kinds: number[]; @@ -69,22 +62,16 @@ export function useTour(identifier: string, pubkey?: string) { filter.authors = [pubkey]; } - console.log(`[useTour] Querying for tour: ${identifier}`); - const events = await querySkateContent([filter], { signal }); - console.log(`[useTour] Received ${events.length} events`); - - if (events.length === 0) { - console.warn(`[useTour] No events found for identifier: ${identifier}`); - return null; - } + if (events.length === 0) return null; return parseSkateTour(events[0]); }, enabled: !!identifier, - staleTime: 30000, - refetchOnMount: 'always', + staleTime: STALE_TIME, + gcTime: GC_TIME, + refetchOnMount: true, }); } @@ -94,7 +81,7 @@ export function useUserTours(pubkey: string) { return useQuery({ queryKey: ['user-skate-tours', pubkey, getQueryKeySuffix()], queryFn: async () => { - const signal = AbortSignal.timeout(10000); + const signal = AbortSignal.timeout(TIMEOUT); const events = await querySkateContent( [ @@ -117,7 +104,8 @@ export function useUserTours(pubkey: string) { return tours.sort((a, b) => b.createdAt - a.createdAt); }, enabled: !!pubkey, - staleTime: 30000, - refetchOnMount: 'always', + staleTime: STALE_TIME, + gcTime: GC_TIME, + refetchOnMount: true, }); } diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx index 3f9bb81..951dd5a 100644 --- a/src/pages/MapPage.tsx +++ b/src/pages/MapPage.tsx @@ -1,8 +1,10 @@ -import { useState, useMemo, useEffect } from 'react'; +import { useState, useMemo, useEffect, lazy, Suspense } from 'react'; import { useSeoMeta } from '@unhead/react'; import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { Layout } from '@/components/Layout'; -import { SpotMap } from '@/components/SpotMap'; + +// Lazy-load the map — Leaflet is large (~150KB) and only needed on this page +const SpotMap = lazy(() => import('@/components/SpotMap').then(m => ({ default: m.SpotMap }))); import { SpotCard, SpotCardSkeleton } from '@/components/SpotCard'; import { WeatherToggle } from '@/components/WeatherToggle'; import { TrustFilter, type TrustFilterLevel, filterByTrustLevel } from '@/components/TrustFilter'; @@ -1061,25 +1063,34 @@ export default function MapPage() {
{viewMode === 'map' ? ( <> - + +
+
+ Loading map... +
+
+ }> + +
{/* Loading indicator */} {isLoading && (
diff --git a/vite.config.ts b/vite.config.ts index 0525a15..cb5f9d9 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -20,7 +20,7 @@ export default defineConfig(() => ({ return !log.includes("React Router Future Flag Warning"); }, env: { - DEBUG_PRINT_LIMIT: '0', // Suppress DOM output that exceeds AI context windows + DEBUG_PRINT_LIMIT: '0', }, }, resolve: { @@ -32,8 +32,55 @@ export default defineConfig(() => ({ exclude: ['@lightninglabs/lnc-web'], }, build: { + // Strip console.log/warn/debug in production (keeps console.error for real errors) + minify: 'esbuild', commonjsOptions: { transformMixedEsModules: true, }, + rollupOptions: { + output: { + // Split large vendor libraries into separate cacheable chunks. + // Users only re-download a chunk when that specific library updates. + manualChunks(id) { + // Map — Leaflet is large (~150KB), isolate it so non-map pages don't pay for it + if (id.includes('leaflet') || id.includes('ngeohash')) { + return 'vendor-map'; + } + // Nostr core — changes infrequently, worth caching separately + if (id.includes('nostr-tools') || id.includes('@nostrify') || id.includes('@noble')) { + return 'vendor-nostr'; + } + // Bitcoin / Lightning — large, rarely changes + if ( + id.includes('@cashu') || + id.includes('bitcoinjs-lib') || + id.includes('@scure') || + id.includes('@bitcoinerlab') || + id.includes('ecpair') || + id.includes('@getalby') || + id.includes('npubcash') + ) { + return 'vendor-bitcoin'; + } + // React ecosystem — very stable, long cache life + if (id.includes('react-dom') || id.includes('react-router')) { + return 'vendor-react'; + } + // Radix UI components — large but stable + if (id.includes('@radix-ui')) { + return 'vendor-radix'; + } + // TanStack Query + if (id.includes('@tanstack')) { + return 'vendor-query'; + } + }, + }, + }, }, -})); \ No newline at end of file + // Strip console.log/warn/debug calls from production build + esbuild: { + drop: ['debugger'], + pure: ['console.log', 'console.warn', 'console.debug'], + }, +}));