Tasks 033 — Frontend performance: route code-splitting and staleTime
Execution skill: superpowers:subagent-driven-development — one implementer per task, then a two-stage review (spec compliance, then code quality). superpowers:test-driven-development applies inside every task: no production code before a failing test that demands it. Reach for superpowers:systematic-debugging on any surprise rather than guessing.
Derived from plan.md (approved). Each task is small, independently verifiable, and reviewed as its own diff. Split where a reviewer could reject one task while approving its neighbour — not where the work merely changes subject. A task is done only when it satisfies the definition of done in CLAUDE.md.
Global Constraints in plan.md apply to every task and are not repeated per task. Two are load-bearing here: no new dependency (everything uses already-installed React/react-router/TanStack Query/Vite), and the React Compiler runs only in vite build — so every task's verification includes pnpm build, not just pnpm test, and lazy wrappers avoid destructured-default + inline-type props.
T1 — Each query carries a staleTime matched to its volatility tier
Satisfies: R3, R4, P2 #1, P2 #2, P2 #3, SC4, SC5.
[ ] RED — write
apps/web/src/api/__tests__/staleTimes.test.ts: assertSTALE_TIME.reference=== 3_600_000,STALE_TIME.pricedStructure === 60_000,STALE_TIME.perUser === 300_000(each a finitenumber > 0), and that anew QueryClient(queryClientConfig)'s resolved defaultqueries.staleTimeis> 0. Run it; watch it fail because the module does not exist yet.[ ] GREEN — create
apps/web/src/api/staleTimes.ts:```ts export const STALE_TIME = { reference: 3_600_000, pricedStructure: 60_000, perUser: 300_000, } as const; ``` In `apps/web/src/api/ApiProvider.tsx`, construct the client with a non-zero default floor — `new QueryClient({ defaultOptions: { queries: { staleTime: 60_000 } } })` — keeping it the one place the client is built (R11). Run the test; it passes.[ ] RED — write
apps/web/src/api/__tests__/staleTimeBehavior.test.tsx(msw + Testing Library): (a) render a facade hook inside aQueryClientProvider, unmount, remount within itsstaleTime, and assert the msw handler fired once (SC5 no-refetch); (b) with a test client whosestaleTimeis tiny, remount after it elapses and assert a second call (P2 #3 staleness still fires); (c) for each of the seven hooks, readqueryClient.getQueryCache().find({ queryKey })?.options.staleTimeand assert it equals the tier from the table below. Run it; watch it fail (hooks pass nostaleTimeyet).[ ] GREEN — add
staleTimeto each facade hook'suseSuspenseQueryoptions (through the existingsuspenseOptionsfacade), per tier:useLegendaries+useHealth→STALE_TIME.reference;useRecipeTree→STALE_TIME.pricedStructure;useAccount+useMaterials+useWallet+useLegendaryRanking→STALE_TIME.perUser. Run the test; it passes.[ ] REFACTOR — only with tests green: confirm no magic number leaked (every value comes from
STALE_TIMEor the QueryClient default), and no hook was left at0.[ ] Teeth — set one per-user hook's
staleTimeback to0; watch the SC5 no-refetch case fail; restore.[ ] Verify + commit —
pnpm typecheck && pnpm test && pnpm build; commit (web: staleTime tiers, no query left at 0).
Verified by: staleTimes.test.ts (SC4: every tier is a finite staleTime > 0; the QueryClient default is > 0) and staleTimeBehavior.test.tsx (SC5: a remount within staleTime fires no request, P2 #3: a query past its staleTime refetches, P2 #2: each hook resolves its tier's staleTime).
T2 — Route pages ship as their own chunks, absent from the entry bundle
Satisfies: R1, R2, P1 #1, P1 #2, P1 #3, SC1, SC2, SC3.
[ ] RED — write
apps/web/src/__tests__/codeSplitting.test.ts: run a production build with Vite's programmatic API (build()withroot=apps/web,build: { manifest: true, outDir= a temp dir},VITE_APP_ENV=prod), read.vite/manifest.json, and assert (a) each feature page —AccountPage,AssistantPage,HealthPage,LegendariesLayout,LegendariesPage,RankingPage,LegendaryDetailPage,MaterialsPage,WalletPage— resolves to its own chunk, and (b) the entry chunk's staticimportscontain none of those page modules. Run it; watch it fail (today everything is one chunk, and the manifest is not emitted). If the in-test build proves flaky under CI limits, fall back to the source-shape guard named inplan.md— do not silently drop the assertion.[ ] GREEN (build config) — add
build: { manifest: true }toapps/web/vite.config.ts.[ ] GREEN (lazy routes) — rewrite each
apps/web/src/features/*/routes.tsxto load its page(s) viaReact.lazy, keeping theelement: <XxxPage />shape so theP1 #4guard still reads aPage/Layoutname (research V3). Pattern, per page:```tsx const XxxPage = lazy(() => import('./XxxPage').then((m) => ({ default: m.XxxPage })), ); ``` Apply to `account`, `assistant`, `health`, `materials`, `wallet`, and all four `legendaries` components (`LegendariesLayout`, `LegendariesPage`, `RankingPage`, `LegendaryDetailPage`). No router `lazy:`/`Component:` props (they bypass the guard). No new `<Suspense>` — the pages suspend into the one in `App.tsx`. Run `codeSplitting.test.ts`; it passes.[ ] GREEN (suspense-aware tests) — a now-lazy page suspends on first render, so update the existing
apps/web/src/features/*/__tests__route/page tests that assumed synchronous render toawait findBy*/await screen.findBy*. Runpnpm test; all green.[ ] Teeth — revert one route to a static
import { XxxPage } from './XxxPage'+element: <XxxPage />; watchcodeSplitting.test.tsfail (that page rejoins the entry chunk); restore the lazy form.[ ] Verify SC3 + build — run
conventions.test.ts: itsP1 #4case still passes and still catches a synthetic violation (unchanged — the routes keptelement: <XxxPage />). Runpnpm buildso the React Compiler sees the lazy wrappers. Commit (web: route-level code-splitting via React.lazy).
Verified by: codeSplitting.test.ts (SC1: no route page module is in the entry chunk, SC2: each route page is its own on-demand chunk) and the unchanged conventions.test.ts (P1 #4: every route element is <Name>Page/<Name>Layout, still catching a violation → SC3).
T3 — Verify the full gate, trace, and hand off
Satisfies: SC7, SC8 (and closes SC6 from research).
- [ ] Run the whole gate:
pnpm typecheck,pnpm lint,pnpm test,pnpm build,pnpm docs:build— all green (SC7). Fix anything red before proceeding; do not weaken a test to pass. - [ ] Fill the traceability table in
spec.mdwith the actual test names from T1/T2 (transcription, not excavation), and confirm SC6 points atresearch.mdV1 + the human's review of the recorded first-paint measurement (no code test — a real-browser metric). - [ ] Update
research.md's Graduation candidates as landed (the warm-origin baseline →docs/gaps/; the staleTime tier table →docs/architecture/react.md) at step 6, or note them explicitly as deferred. - [ ] Hand off for review (
superpowers:requesting-code-review). The human reviews the diff and, before merge, transcribesspec.mdstatusapproved→implementedinside the PR diff (the agent never sets it unbidden).
Verified by: the five gate commands all exit green (SC7); the spec.md traceability table names a real test for every acceptance scenario and success criterion (SC8).
Notes
Staging area for decisions and surprises found during implementation — including anything that turned out differently from what plan.md assumed. Move each one into spec.md, research.md, or docs/ before closing the feature; this section is not a home.
- Watch the entry-chunk before/after in
codeSplitting's build output and record the delta inresearch.md(evidence for SC1; no absolute-KB assertion — the entry is vendor-dominated, research F1). - If a facade hook's
queryKeyis not stable enough tofind()instaleTimeBehavior.test.tsx(the per-user hooks hash the API key), assert against the key the hook actually builds rather than a guessed one.