Files
TREK/client/src/main.tsx
T
Maurice 96e4ff5932 perf(client): load the page chunks on demand (#1812)
* feat(client): retry a failed page chunk once before giving up

A dynamic import fails for two different reasons, and only one of them is
worth a second attempt. If the file is gone because a deploy happened while
the tab was open, nothing but a reload helps and reloadOnceForChunk already
owns that path. If the fetch itself went wrong — hotel wifi, an aborted
request, a service worker handing back a truncated entry — one retry fixes
it.

The retry cannot reuse the original specifier, since Vite rewrites it into a
hashed asset URL at build time; the only usable URL is the one the browser
names in the failure message, and a fresh query on it misses both the HTTP
cache and the Workbox precache.

This is also where a healthy load is acknowledged. Clearing the reload marker
used to sit in main.tsx right after render(), which runs before React commits
and long before any chunk is fetched, so the marker was gone before it was
ever needed.

* perf(client): load the page chunks on demand

Every one of the twenty pages was a static import, so the entry chunk carried
the planner, the journal, the atlas and the vacation planner for anyone who
only opened the dashboard. They now load through lazyWithRetry; the entry
chunk drops from 5,183 kB to 1,243 kB (gzip 1,300 -> 349).

LoginPage stays static on purpose: anyone logged out lands there, and every
other route redirects there first.

The Suspense boundary sits above <Routes> rather than inside ProtectedRoute
so it stays mounted across navigations. react-router runs location updates
inside a transition, which keeps the current page on screen while the next
chunk arrives instead of flashing a spinner on every jump.

The precache is unchanged by design: workbox picks up the new chunks, so the
install size stays where it was (107 -> 178 entries, +31 KiB) and every route
remains available offline. This buys first paint, not install size.
2026-08-07 10:22:51 +02:00

59 lines
2.9 KiB
TypeScript

import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router'
import App from './App'
// Self-hosted Poppins (bundled, same-origin) so the app font can't be blocked by
// ad/tracker blockers the way the Google Fonts CDN can.
import '@fontsource/poppins/300.css'
import '@fontsource/poppins/400.css'
import '@fontsource/poppins/500.css'
import '@fontsource/poppins/600.css'
import '@fontsource/poppins/700.css'
// MuseoModerno for the login wordmark — same reasoning, it used to come from the
// Google Fonts CDN via a render-blocking <link> in index.html.
import '@fontsource/museomoderno/400.css'
import '@fontsource/museomoderno/700.css'
import '@fontsource/museomoderno/800.css'
// Geist Sans (self-hosted too) — used only for secondary "subtext" via --font-subtext.
import '@fontsource/geist-sans/400.css'
import '@fontsource/geist-sans/500.css'
import '@fontsource/geist-sans/600.css'
// Leaflet CSS bundled from node_modules instead of unpkg: the service worker
// cached the CDN stylesheet as an opaque response, which the browser then
// rejected, breaking the Atlas/trip maps (#1497). Bundling keeps it same-origin
// and precached with the app shell. Must stay above index.css, which overrides
// several .leaflet-* styles.
import 'leaflet/dist/leaflet.css'
import './index.css'
// Native HTML5 drag-and-drop never fires on touch input, so the planner's place /
// day reordering was dead on Android and iOS. The `drag-drop-touch` polyfill synthesises
// the standard drag events from touch gestures over draggable elements (#1265). It is
// loaded only on non-mobile viewports: reorder DnD is disabled on mobile (#1432), where
// the polyfill's synthetic click/dblclick otherwise turned quick one-finger map pans into
// double-tap zooms (#1440). See utils/touchDragPolyfill.ts.
import { maybeInstallTouchDragPolyfill } from './utils/touchDragPolyfill'
import { startConnectivityProbe } from './sync/connectivity'
import { requestPersistentStorage } from './sync/persistentStorage'
import ErrorBoundary, { RootErrorFallback } from './components/shared/ErrorBoundary'
import { installGlobalErrorHandlers } from './utils/globalErrorHandlers'
maybeInstallTouchDragPolyfill()
startConnectivityProbe()
// Keep offline data (map tiles, file blobs, IndexedDB) exempt from eviction.
requestPersistentStorage()
// Event handlers and async code never reach a boundary; this is where they land.
installGlobalErrorHandlers()
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
{/* Outermost net: catches App itself and TranslationProvider, which every
boundary further in sits below. Its fallback is untranslated on purpose
— if the provider is the thing that broke, t() would echo raw keys. */}
<ErrorBoundary boundaryId="root" level="root" fallback={s => <RootErrorFallback {...s} />}>
<App />
</ErrorBoundary>
</BrowserRouter>
</React.StrictMode>,
)