feat(frontend): route-level code splitting (#105) #183

Merged
claude-bot merged 1 commit from feat/105-route-code-splitting into main 2026-07-17 02:24:30 +00:00
Contributor

Fixes #105. Third of the v3.5.0 (Frontend Platform & PWA) milestone, after #180 and #181.

Why

App.jsx eagerly imported all 20 pages, so every route shipped in one 685 kB chunk. A player opening the dashboard downloaded the admin panel and the entire react-markdown/micromark toolchain — pulled in by WikiArticle, its sole consumer — before seeing anything.

What changed

  • Page imports → React.lazy, with a single <Suspense> around the route table. AuthGuard still gates protected routes, so a page chunk is only fetched once its route matches.
  • Login stays eager — judgement call, documented in the file. It's small, and AuthGuard bounces every logged-out visitor straight to it as soon as the /api/me probe resolves; splitting it would add a chunk round-trip and a fallback flash to the most common cold-start path.
  • The Suspense fallback reuses AuthGuard's existing loading UI rather than inventing a second spinner. That markup moved to components/LoadingScreen.jsx; both share it now.

Before / after

before after
Entry JS 685.50 kB (gzip 172.36 kB) 244.25 kB (gzip 78.32 kB)
JS chunks 1 1 entry + 23 lazy
rollup >500 kB warning yes gone

−441 kB (−64%) raw, −94 kB (−55%) gzipped off the initial download.

The markdown toolchain is now isolated in the 169.21 kB WikiArticle chunk (gzip 46.42 kB), fetched only by people who actually open a wiki article. Other notable splits: CampaignDetail 67.73 kB, Admin 42.91 kB, SessionDetail 31.37 kB, Dashboard just 10.56 kB.

Acceptance criteria

  • Initial JS payload for the dashboard route drops substantially — 685.50 → 244.25 kB entry (gzip 172.36 → 78.32); a dashboard visitor now adds only the 10.56 kB Dashboard chunk plus small shared api chunks, instead of the whole app.

  • react-markdown is absent from the entry chunk — verified by grepping the built assets, not assumed:

    marker entry chunk WikiArticle chunk
    micromark, mdast, hast, remark, fromMarkdown, characterReference 0 present
  • All page tests pass with lazy routes — 97/97. No harness changes needed; the suite already awaits its queries, so lazy resolution didn't break it.

  • Navigating to every route works in a production build (vite preview smoke check)20/20 routes serve 200, 24/24 built assets serve 200, and index.html references the entry chunk.

Verification

Run in node:20, matching CI: npm run lint → exit 0, no output. npx vitest run → 97/97. npx vite build → succeeds, no chunk-size warning.

Honest limitation on the smoke check: it runs vite preview and asserts every route and every built asset serves 200 — it proves the SPA and all 23 lazy chunks are served and reachable, and that no lazy import path is broken. It does not execute JS in a real browser (no headless browser in the toolchain). The 97 jsdom tests do render the app through the lazy boundary, which covers the execution side.

🤖 Generated with Claude Code

Fixes #105. Third of the v3.5.0 (Frontend Platform & PWA) milestone, after #180 and #181. ## Why `App.jsx` eagerly imported all 20 pages, so every route shipped in **one 685 kB chunk**. A player opening the dashboard downloaded the admin panel and the entire `react-markdown`/micromark toolchain — pulled in by `WikiArticle`, its sole consumer — before seeing anything. ## What changed - Page imports → `React.lazy`, with a single `<Suspense>` around the route table. `AuthGuard` still gates protected routes, so a page chunk is only fetched once its route matches. - **`Login` stays eager** — judgement call, documented in the file. It's small, and `AuthGuard` bounces every logged-out visitor straight to it as soon as the `/api/me` probe resolves; splitting it would add a chunk round-trip *and* a fallback flash to the most common cold-start path. - The Suspense fallback **reuses AuthGuard's existing loading UI** rather than inventing a second spinner. That markup moved to `components/LoadingScreen.jsx`; both share it now. ## Before / after | | before | after | |---|---|---| | Entry JS | **685.50 kB** (gzip 172.36 kB) | **244.25 kB** (gzip **78.32 kB**) | | JS chunks | 1 | 1 entry + **23 lazy** | | rollup >500 kB warning | yes | **gone** | **−441 kB (−64%) raw, −94 kB (−55%) gzipped** off the initial download. The markdown toolchain is now isolated in the **169.21 kB** `WikiArticle` chunk (gzip 46.42 kB), fetched only by people who actually open a wiki article. Other notable splits: `CampaignDetail` 67.73 kB, `Admin` 42.91 kB, `SessionDetail` 31.37 kB, `Dashboard` just **10.56 kB**. ## Acceptance criteria - [x] **Initial JS payload for the dashboard route drops substantially** — 685.50 → 244.25 kB entry (gzip 172.36 → 78.32); a dashboard visitor now adds only the 10.56 kB `Dashboard` chunk plus small shared api chunks, instead of the whole app. - [x] **`react-markdown` is absent from the entry chunk** — verified by grepping the *built* assets, not assumed: | marker | entry chunk | WikiArticle chunk | |---|---|---| | `micromark`, `mdast`, `hast`, `remark`, `fromMarkdown`, `characterReference` | **0** | present | - [x] **All page tests pass with lazy routes** — 97/97. No harness changes needed; the suite already awaits its queries, so lazy resolution didn't break it. - [x] **Navigating to every route works in a production build (`vite preview` smoke check)** — **20/20 routes serve 200**, **24/24 built assets serve 200**, and `index.html` references the entry chunk. ## Verification Run in **node:20**, matching CI: `npm run lint` → exit 0, no output. `npx vitest run` → 97/97. `npx vite build` → succeeds, no chunk-size warning. **Honest limitation on the smoke check:** it runs `vite preview` and asserts every route and every built asset serves 200 — it proves the SPA and all 23 lazy chunks are served and reachable, and that no lazy import path is broken. It does *not* execute JS in a real browser (no headless browser in the toolchain). The 97 jsdom tests do render the app through the lazy boundary, which covers the execution side. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(frontend): route-level code splitting (#105)
All checks were successful
CI / Frontend tests, audit, and build (pull_request) Successful in 1m4s
CI / Backend lint (ruff) (pull_request) Successful in 28s
CI / Docker image build (pull_request) Successful in 20s
CI / Backend migration, tests, and audit (pull_request) Successful in 3m32s
CI / Bot tests and audit (pull_request) Successful in 6m16s
2c7b4fe642
App.jsx eagerly imported all 20 pages, so every route shipped in one 685 kB
chunk. A player opening the dashboard downloaded the admin panel and the whole
react-markdown/micromark toolchain (pulled in by WikiArticle, its sole consumer)
before seeing anything.

Convert the page imports to React.lazy and wrap the route table in a single
Suspense boundary. AuthGuard still gates protected routes, so a page chunk is
only fetched once its route matches.

Login stays eager: it is small, and AuthGuard bounces every logged-out visitor
straight to it once the /api/me probe resolves, so splitting it would add a
chunk round-trip and a fallback flash to the most common cold-start path.

The Suspense fallback reuses AuthGuard's existing loading UI rather than
inventing a second spinner; that markup moves into components/LoadingScreen.jsx
and both now share it.

Entry chunk:

    before   685.50 kB  (gzip 172.36 kB)   1 JS chunk
    after    244.25 kB  (gzip  78.32 kB)   + 23 lazy chunks

-441 kB / -64% raw, -94 kB / -55% gzipped, and rollup's >500 kB chunk warning is
gone. The markdown toolchain is now isolated in the 169.21 kB WikiArticle chunk,
downloaded only by wiki readers — verified by grepping the built assets:
micromark/mdast/hast/remark appear in the WikiArticle chunk and are absent from
the entry chunk.

No test-harness changes were needed; the suite already awaits its queries, so
lazy resolution didn't break it.

Verified in node:20: lint clean, 97/97 tests, and a vite preview smoke check of
the production build — all 20 routes serve 200 and all 24 built assets serve 200.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude-bot deleted branch feat/105-route-code-splitting 2026-07-17 02:24:31 +00:00
Sign in to join this conversation.
No description provided.