perf(web): lazy-load page routes + prefetch-on-hover

Switch all navigable route components to React.lazy() and wrap
<Routes> in <Suspense>. Dashboard/Login/Layout stay eager since
they're the shell.

Initial index bundle drops 246kB -> 34.67kB (gzip 10.5kB). Each
route becomes its own 8-51kB chunk, loaded on demand.

Nav hover/focus triggers prefetchRoute(path) which fires the same
dynamic import() specifier the bundler dedups against React.lazy,
so the chunk is warm by the time the user clicks. Avoids the
Suspense flicker that would otherwise show on every first nav.
This commit is contained in:
2026-04-24 18:38:26 -04:00
parent 7389ddb62c
commit 52cbb01555
3 changed files with 97 additions and 28 deletions

View File

@@ -0,0 +1,39 @@
/* Prefetch-on-intent for lazy-loaded routes.
*
* Each key is a route path; each value is the same dynamic import()
* used by React.lazy() in App.tsx. The bundler dedups by specifier
* string, so a hover-triggered import here warms the exact chunk
* React.lazy resolves on click — no double fetch, no separate chunk.
*
* A Set of already-fired paths prevents redundant imports on repeat
* hovers; the module cache would short-circuit anyway, but skipping
* the call avoids a microtask and makes intent obvious in devtools. */
type Loader = () => Promise<unknown>;
const loaders: Record<string, Loader> = {
'/fleet': () => import('./components/DeckyFleet'),
'/mazenet': () => import('./components/MazeNET/MazeNET'),
'/topologies': () => import('./components/TopologyList/TopologyList'),
'/live-logs': () => import('./components/LiveLogs'),
'/webhooks': () => import('./components/Webhooks'),
'/bounty': () => import('./components/Bounty'),
'/attackers': () => import('./components/Attackers'),
'/config': () => import('./components/Config'),
'/swarm-updates': () => import('./components/RemoteUpdates'),
'/swarm/hosts': () => import('./components/SwarmHosts'),
};
const fired = new Set<string>();
export function prefetchRoute(path: string): void {
const loader = loaders[path];
if (!loader || fired.has(path)) return;
fired.add(path);
loader().catch(() => {
// Network hiccup on a speculative prefetch is a non-event —
// React.lazy will re-try on actual navigation and surface the
// real error there if it persists.
fired.delete(path);
});
}