1+ import { lazy , Suspense } from 'react'
12import { BrowserRouter , Navigate , Route , Routes , useLocation } from 'react-router-dom'
2- import { AppShell } from './layout/AppShell'
33
4- // Public marketing surfaces
4+ // Public marketing surfaces — eagerly imported. A marketing visitor might
5+ // click any of these from the homepage nav, so keeping them in the main
6+ // chunk avoids a network round-trip on first interaction. These are also
7+ // the routes that get statically pre-rendered by scripts/prerender.mjs.
58import { MarketingPage } from './pages/MarketingPage'
69import { PricingPage } from './pages/PricingPage'
710import { ForAgentsPage } from './pages/ForAgentsPage'
@@ -12,24 +15,65 @@ import { DocsPage } from './pages/DocsPage'
1215import { UseCasesPage } from './pages/UseCasesPage'
1316import { UseCaseDetailPage } from './pages/UseCaseDetailPage'
1417
15- // Auth surfaces
18+ // Auth surfaces — eagerly imported. A marketing visitor can land on /login
19+ // directly (deep link, "Sign in" button), and the login form is small.
1620import { LoginPage } from './pages/LoginPage'
1721import { LoginCallbackPage } from './pages/LoginCallbackPage'
1822import { ClaimPage } from './pages/ClaimPage'
1923
20- // Authenticated dashboard surfaces
21- import { OverviewPage } from './pages/OverviewPage'
22- import { ResourcesPage } from './pages/ResourcesPage'
23- import { ResourceDetailPage } from './pages/ResourceDetailPage'
24- import { DeploymentsPage } from './pages/DeploymentsPage'
25- import { DeployDetailPage } from './pages/DeployDetailPage'
26- import { StacksPage } from './pages/StacksPage'
27- import { VaultPage } from './pages/VaultPage'
28- import { TeamPage } from './pages/TeamPage'
29- import { BillingPage } from './pages/BillingPage'
30- import { SettingsPage } from './pages/SettingsPage'
31- import { AgentPage } from './pages/AgentPage'
32- import { ContractsPage } from './pages/ContractsPage'
24+ // Authenticated dashboard surfaces — lazy-loaded. These pages only render
25+ // behind AuthGate (token must be present), so a marketing visitor never
26+ // needs the bytes. Each React.lazy() call ends up in its own chunk; Rollup
27+ // splits these out of the main bundle and the browser fetches them on
28+ // demand when the user navigates into /app/*.
29+ //
30+ // AppShell is also lazy-loaded because it's exclusively the chrome for
31+ // /app/* — the nav rail, breadcrumbs, scope pills. A marketing visitor on
32+ // the homepage never sees it, so its ~10 KB of JSX + the useDashboardCtx
33+ // hook tree it pulls in don't need to ship in the entry bundle.
34+ //
35+ // All page components use named exports, so we adapt them to React.lazy's
36+ // default-export contract inline. The chunkName comment is a hint for
37+ // rollup so the emitted file has a recognizable name in dist/assets/.
38+ const AppShell = lazy ( ( ) =>
39+ import ( './layout/AppShell' ) . then ( ( m ) => ( { default : m . AppShell } ) ) ,
40+ )
41+ const OverviewPage = lazy ( ( ) =>
42+ import ( /* webpackChunkName: "app-overview" */ './pages/OverviewPage' ) . then ( ( m ) => ( { default : m . OverviewPage } ) ) ,
43+ )
44+ const ResourcesPage = lazy ( ( ) =>
45+ import ( './pages/ResourcesPage' ) . then ( ( m ) => ( { default : m . ResourcesPage } ) ) ,
46+ )
47+ const ResourceDetailPage = lazy ( ( ) =>
48+ import ( './pages/ResourceDetailPage' ) . then ( ( m ) => ( { default : m . ResourceDetailPage } ) ) ,
49+ )
50+ const DeploymentsPage = lazy ( ( ) =>
51+ import ( './pages/DeploymentsPage' ) . then ( ( m ) => ( { default : m . DeploymentsPage } ) ) ,
52+ )
53+ const DeployDetailPage = lazy ( ( ) =>
54+ import ( './pages/DeployDetailPage' ) . then ( ( m ) => ( { default : m . DeployDetailPage } ) ) ,
55+ )
56+ const StacksPage = lazy ( ( ) =>
57+ import ( './pages/StacksPage' ) . then ( ( m ) => ( { default : m . StacksPage } ) ) ,
58+ )
59+ const VaultPage = lazy ( ( ) =>
60+ import ( './pages/VaultPage' ) . then ( ( m ) => ( { default : m . VaultPage } ) ) ,
61+ )
62+ const TeamPage = lazy ( ( ) =>
63+ import ( './pages/TeamPage' ) . then ( ( m ) => ( { default : m . TeamPage } ) ) ,
64+ )
65+ const BillingPage = lazy ( ( ) =>
66+ import ( './pages/BillingPage' ) . then ( ( m ) => ( { default : m . BillingPage } ) ) ,
67+ )
68+ const SettingsPage = lazy ( ( ) =>
69+ import ( './pages/SettingsPage' ) . then ( ( m ) => ( { default : m . SettingsPage } ) ) ,
70+ )
71+ const AgentPage = lazy ( ( ) =>
72+ import ( './pages/AgentPage' ) . then ( ( m ) => ( { default : m . AgentPage } ) ) ,
73+ )
74+ const ContractsPage = lazy ( ( ) =>
75+ import ( './pages/ContractsPage' ) . then ( ( m ) => ( { default : m . ContractsPage } ) ) ,
76+ )
3377
3478import { getToken } from './api'
3579
@@ -42,6 +86,24 @@ function AuthGate({ children }: { children: JSX.Element }) {
4286 return children
4387}
4488
89+ // AppLoadingFallback — shown while a lazy-loaded /app/* chunk is in flight.
90+ // Tiny inline style so it renders even before the page's own CSS resolves.
91+ // In practice this fallback is on screen for ~50-150ms on a warm cache.
92+ function AppLoadingFallback ( ) {
93+ return (
94+ < div
95+ style = { {
96+ padding : '2rem' ,
97+ fontFamily : 'system-ui, sans-serif' ,
98+ color : 'var(--text-muted, #888)' ,
99+ fontSize : '0.875rem' ,
100+ } }
101+ >
102+ Loading…
103+ </ div >
104+ )
105+ }
106+
45107// PricingPage and ForAgentsPage both wrap themselves in <PublicShell>, and
46108// MarketingPage inlines its own nav. So routes mount the page directly —
47109// no extra shell wrapper needed (would cause double nav rendering).
@@ -50,6 +112,12 @@ function AuthGate({ children }: { children: JSX.Element }) {
50112// the SSR entry (src/entry-server.tsx) can mount it under <StaticRouter>
51113// for build-time pre-rendering. The browser-side wrapper below stays the
52114// same — this is just an extraction, no route changes.
115+ //
116+ // SSR note: scripts/prerender.mjs only renders public routes (see its
117+ // PRERENDER_ROUTES list — no /app/* paths). React.lazy resolves to a
118+ // Suspense fallback during SSR for unrendered chunks, but since SSG never
119+ // visits an /app route, the lazy components are never invoked server-side
120+ // and the build still emits 115 HTML files.
53121export function AppRoutes ( ) {
54122 return (
55123 < Routes >
@@ -70,11 +138,17 @@ export function AppRoutes() {
70138 < Route path = "/claim" element = { < ClaimPage /> } />
71139
72140 { /* ─── authenticated dashboard at /app/* ─────────────────── */ }
141+ { /* Suspense wraps the whole /app subtree so any lazy page below
142+ shows the same fallback while its chunk loads. We place it
143+ inside AuthGate so unauthenticated users redirect to /login
144+ without ever triggering a chunk fetch. */ }
73145 < Route
74146 path = "/app"
75147 element = {
76148 < AuthGate >
77- < AppShell />
149+ < Suspense fallback = { < AppLoadingFallback /> } >
150+ < AppShell />
151+ </ Suspense >
78152 </ AuthGate >
79153 }
80154 >
0 commit comments