From 11cfb6f999832dde40038174d3fa574ec4cfbfef Mon Sep 17 00:00:00 2001 From: Julius Henke Date: Mon, 12 Oct 2026 22:48:49 +0200 Subject: [PATCH] perf(frontend): load the map tiles without waiting for the overlay and dialogs The map, the overlay and the attribution shared one Suspense boundary, so no tile was requested before the date picker and icon chunks were loaded and rendered (about 500 ms of the LCP). The map is now part of the entry, the overlay has its own boundary, and the relay and family dialogs are only loaded when they are opened for the first time. Lighthouse (local build, mock API, median of 5): LCP desktop 1390 -> 995 ms, mobile 6230 -> 4476 ms. Co-Authored-By: Claude Sonnet 5.5 --- frontend/src/components/App.tsx | 61 ++++++++++--------- .../relay/ResponsiveRelayDetailsDialog.tsx | 7 ++- .../src/components/leaflet/LeafletLayers.tsx | 18 ++++-- 3 files changed, 49 insertions(+), 37 deletions(-) diff --git a/frontend/src/components/App.tsx b/frontend/src/components/App.tsx index 0d448055..6171393f 100644 --- a/frontend/src/components/App.tsx +++ b/frontend/src/components/App.tsx @@ -6,13 +6,14 @@ import {useDate} from "../context/date-context"; import {SnackbarMessage} from "../types/ui"; import {backend} from "../util/util"; import {AboutInformation} from "./dialogs/AboutInformation"; +import {LeafletWorldMap} from "./leaflet/LeafletWorldMap"; +import {ExternalLink} from "./link/ExternalLink"; import {LoadingAnimation} from "./loading/LoadingAnimation"; -// Lazy loaded components -const ExternalLink = React.lazy(() => import('./link/ExternalLink')); +// Lazy loaded components. The map is part of the entry (it is always needed), so its chunk is requested together with +// the entry script instead of after it. The overlay has its own Suspense boundary, so the map does not wait for it. const OverlayLarge = React.lazy(() => import('./overlay/OverlayLarge')); const OverlaySmall = React.lazy(() => import('./overlay/OverlaySmall')); -const LeafletWorldMap = React.lazy(() => import('./leaflet/LeafletWorldMap')); export const App: FunctionComponent = () => { // Component state @@ -46,34 +47,34 @@ export const App: FunctionComponent = () => { return ( <> - }> - - {isLoading ? : null} - - - + + {isLoading ? : null} + + + + {isLargeScreen ? : } - - - - {" | © "} - - {" contributors © "} - - - - - - + + + + + {" | © "} + + {" contributors © "} + + + + + ) } diff --git a/frontend/src/components/dialogs/relay/ResponsiveRelayDetailsDialog.tsx b/frontend/src/components/dialogs/relay/ResponsiveRelayDetailsDialog.tsx index 6c86dec1..f94039ec 100644 --- a/frontend/src/components/dialogs/relay/ResponsiveRelayDetailsDialog.tsx +++ b/frontend/src/components/dialogs/relay/ResponsiveRelayDetailsDialog.tsx @@ -114,8 +114,11 @@ export const ResponsiveRelayDetailsDialog: FunctionComponent = () => { } } - // Discard the previous relays as soon as the relays of another location are passed to the dialog - const [syncedRelayDetailsIdToLocationMap, setSyncedRelayDetailsIdToLocationMap] = useState(relayDetailsIdToLocationMap) + // Discard the previous relays as soon as the relays of another location are passed to the dialog. + // Starts without a value, because the dialog is only mounted when it is opened for the first time, so the relays of + // that first location have to count as a change too. + const [syncedRelayDetailsIdToLocationMap, setSyncedRelayDetailsIdToLocationMap] = + useState>() if (syncedRelayDetailsIdToLocationMap !== relayDetailsIdToLocationMap) { setSyncedRelayDetailsIdToLocationMap(relayDetailsIdToLocationMap) setRelayDetailsMatch(undefined) diff --git a/frontend/src/components/leaflet/LeafletLayers.tsx b/frontend/src/components/leaflet/LeafletLayers.tsx index c17061a5..f8d245df 100644 --- a/frontend/src/components/leaflet/LeafletLayers.tsx +++ b/frontend/src/components/leaflet/LeafletLayers.tsx @@ -56,16 +56,22 @@ export const LeafletLayers: FunctionComponent = ({relays, reloadSelectedD // Component state const [showFamilySelectionDialog, setShowFamilySelectionDialog] = useState(false) const [familiesForSelectionDialog, setFamiliesForSelectionDialog] = useState([]) + // The dialogs are separate chunks and are only loaded when they are opened for the first time. They stay mounted + // afterwards, so they can close with their transition. + const [hasOpenedRelayDetailsDialog, setHasOpenedRelayDetailsDialog] = useState(false) + const [hasOpenedFamilySelectionDialog, setHasOpenedFamilySelectionDialog] = useState(false) const [leafletMarkerLayers] = useState(new LayerGroup()) // The country borders are a separate chunk, which is only loaded once the countries are shown const [worldFeatures, setWorldFeatures] = useState() // Atom state - const [, setShowRelayDetailsDialog] = useAtom(showRelayDetailsDialogAtom) + const [showRelayDetailsDialog, setShowRelayDetailsDialog] = useAtom(showRelayDetailsDialogAtom) const [, setRelaysForDetailsDialog] = useAtom(relaysForDetailsDialogAtom) const [, setRelayDetailsDialogSearch] = useAtom(relayDetailsDialogSearchAtom) const [, setFilteredRelays] = useAtom(filteredRelaysAtom) const [relaySearchOnMap, setRelaySearchOnMap] = useAtom(relaySearchOnMapAtom) + if (showRelayDetailsDialog && !hasOpenedRelayDetailsDialog) setHasOpenedRelayDetailsDialog(true) + if (showFamilySelectionDialog && !hasOpenedFamilySelectionDialog) setHasOpenedFamilySelectionDialog(true) // App context const leafletMap = useMap() @@ -251,15 +257,17 @@ export const LeafletLayers: FunctionComponent = ({relays, reloadSelectedD } }, [filteredRelays, setFilteredRelays, leafletMarkerLayers, relays, statistics, setStatistics, filteredRelaysByFlags.length, settings, relayFamilyMap, leafletMap, relayCountryMap, closeSnackbar, enqueueSnackbar, setSettings, relayLocationHeatmapLayer, countryBordersLayer, aggregatedCoordinatesLayer, relayCountryLayer, relayLayer, relaySelectedFamilyLayer, relayFamilyCoordinatesLayer]) + const closeFamilySelectionDialog = useCallback(() => setShowFamilySelectionDialog(false), []) + return ( }> - - : null} + {hasOpenedFamilySelectionDialog ? setShowFamilySelectionDialog(false), [])} + closeDialog={closeFamilySelectionDialog} reloadSelectedDay={reloadSelectedDay} familyIds={familiesForSelectionDialog} - /> + /> : null} ); };