Commit 03bcc10
perf(dashboard): lazy-load secondary marketing pages to shrink main bundle (#27)
PR #19 already split /app/* routes out of the entry chunk, but the
remaining public marketing pages were still eagerly imported in App.tsx
on the theory that "a homepage visitor might click any of them." In
practice, BlogPostPage / DocsPage / UseCaseDetailPage etc. are only
reachable after at least one click, so paying for their bytes on cold
load is wasted weight on the most-visited path.
This commit converts the secondary public pages to React.lazy() and
relies on Rollup's per-import code-splitting to peel them into their
own chunks. The first-paint JS that a homepage visitor downloads is
now meaningfully smaller; chunks for /pricing, /docs, /blog, /status,
/use-cases, /for-agents are fetched on demand when the visitor clicks
into those routes.
Bundle sizes (vite build), main entry + each new chunk:
| File | Before | After |
|-------------------------------------|-----------------|-----------------|
| index-*.js (main entry) | 710.87 / 194.51 | 616.57 / 168.76 |
| PricingPage chunk | (in main) | 14.43 / 3.87 |
| ForAgentsPage chunk | (in main) | 11.43 / 3.53 |
| StatusPage chunk | (in main) | 8.67 / 2.73 |
| BlogPage chunk | (in main) | 2.22 / 0.97 |
| BlogPostPage chunk | (in main) | 3.06 / 1.14 |
| DocsPage chunk | (in main) | 10.48 / 4.49 |
| UseCasesPage chunk | (in main) | 5.07 / 1.88 |
| UseCaseDetailPage chunk | (in main) | 9.63 / 2.90 |
| PublicShell chunk (extracted) | (in main) | 9.02 / 2.32 |
| posts content chunk | (in main) | 22.68 / 9.95 |
| markdown renderer chunk | (in main) | 1.98 / 0.84 |
(All sizes in kB; pair is raw / gzip. Vite's 500 kB chunk-size warning
still fires on the main entry — most of the remaining bytes are react +
react-dom + react-router + MarketingPage's inlined USE_CASES dataset.
That's tracked separately.)
Net delta on cold load:
raw: 710.87 → 616.57 kB (-94.30 kB, -13.3%)
gzip: 194.51 → 168.76 kB (-25.75 kB, -13.2%)
SSG complication and how it's handled:
scripts/prerender.mjs renders public routes through renderToString at
build time so crawlers see real HTML, not an empty <div id="root">.
React.lazy() during renderToString resolves to the parent Suspense
fallback, NOT the page's content — so naively lazy-loading the public
pages would mean every pre-rendered HTML (/pricing, /docs, /blog/*,
/use-cases/*, …) ships only a <span aria-hidden> instead of the
marketing copy. That defeats the entire SEO/GEO setup.
Fix: src/entry-server.tsx now declares its own SSRRoutes with
synchronous imports for every public page. The SSR bundle is built by
Vite as a separate module graph (build({ ssr: 'src/entry-server.tsx' }))
and thrown away after prerender, so the static imports there don't bloat
the client output. The route table is duplicated between App.tsx (lazy)
and entry-server.tsx (static) — small cost, documented inline at the top
of entry-server.tsx, and Keep-In-Sync flagged for future route adds.
I first tried a unified App.tsx using `import.meta.env.SSR` to switch
between lazy and static imports inline. Vite emits a warning when a
module is both statically and dynamically imported and refuses to split
it — so that approach silently put every "lazy" page back into the main
bundle. Separate SSR entry is the only pattern that actually splits.
Verification:
- npm run build emits 116 static HTML files + 116 .md mirrors (unchanged).
- Pre-rendered HTML contains real content (no "Loading…" fallbacks):
/pricing 36 kB, /docs 22 kB, /use-cases 73 kB, /blog 14 kB.
- npm test → 96 passed, 3 skipped (unchanged from baseline).
- tsc --noEmit clean.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent e94748f commit 03bcc10
2 files changed
Lines changed: 114 additions & 26 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
5 | | - | |
6 | | - | |
7 | | - | |
| 4 | + | |
| 5 | + | |
8 | 6 | | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | | - | |
15 | | - | |
16 | | - | |
17 | 7 | | |
18 | 8 | | |
19 | 9 | | |
20 | 10 | | |
21 | 11 | | |
22 | 12 | | |
23 | 13 | | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
24 | 49 | | |
25 | 50 | | |
26 | 51 | | |
| |||
104 | 129 | | |
105 | 130 | | |
106 | 131 | | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
107 | 145 | | |
108 | 146 | | |
109 | 147 | | |
110 | 148 | | |
111 | | - | |
112 | | - | |
113 | | - | |
114 | | - | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
115 | 152 | | |
116 | | - | |
117 | | - | |
118 | | - | |
119 | | - | |
120 | | - | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
121 | 157 | | |
122 | 158 | | |
123 | | - | |
| 159 | + | |
| 160 | + | |
124 | 161 | | |
125 | 162 | | |
126 | 163 | | |
| |||
180 | 217 | | |
181 | 218 | | |
182 | 219 | | |
| 220 | + | |
183 | 221 | | |
184 | 222 | | |
185 | 223 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
10 | | - | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
11 | 28 | | |
12 | 29 | | |
| 30 | + | |
13 | 31 | | |
14 | 32 | | |
15 | | - | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
16 | 66 | | |
17 | 67 | | |
18 | 68 | | |
19 | 69 | | |
20 | 70 | | |
21 | | - | |
| 71 | + | |
22 | 72 | | |
23 | 73 | | |
24 | 74 | | |
| |||
0 commit comments