Frontend Performance Baseline — 2026-08-07 (Phase 3D, slice 1)
Scope: miz-oki-command-center-ui — bundle-analysis wiring, first code-splitting
pass on heavy visualization routes, fleet-grid virtualization decision, and the
route-size budget this slice establishes.
Truth discipline (TRUTH.md): every number below is a measurement of a local
production build (npm run build, Next.js 15.5.21, standalone output, built in
the working tree on 2026-08-07). These are not live-infrastructure measurements
— nothing here upgrades to a "verified result" serving claim until the same build
is deployed and measured on Cloud Run. The budget in §5 is a rule going forward,
not an observed guarantee.
1. Method
- Baseline build:
npm run buildon the pre-change tree (exit 0), route table captured from the build output ("First Load JS" per route + shared chunks). - Changes applied: analyzer wiring (§2),
next/dynamicat four page-level import sites (§4), no edits to the heavy components themselves. - After build:
npm run buildagain (exit 0), same capture. - Noise floor: the two builds ran ~40 minutes apart in a shared working tree
while two sibling work streams edited disjoint zones. Every route not touched
by this slice moved by exactly +1 kB (the shared baseline went 103 kB →
104 kB, "other shared chunks" +0.9 kB — the
next/dynamiclazy-loading runtime entering the shared graph, plus any concurrent sibling edits). Deltas of ±1 kB are therefore noise; the changed-route deltas below are 40–480× that floor. - Route count: 399 app routes in both tables — none added or removed.
2. Bundle analyzer (opt-in)
@next/bundle-analyzer (already in devDependencies) is wired in
miz-oki-command-center-ui/next.config.mjs behind ANALYZE=1:
ANALYZE=1 npm run build # writes treemap reports under .next/analyze/
npm run build # ANALYZE unset -> wrapper disabled, config untouched
Measured 2026-08-07: with ANALYZE unset the exported config's keys are
byte-identical to the pre-wrapper config (no webpack hook injected); with
ANALYZE=1 the analyzer's webpack hook is present. The Phase-3F security
headers block is preserved verbatim.
3. Changed routes — before / after (measured local build)
First Load JS = what a cold client downloads to render the route.
| Route | First Load JS before | First Load JS after | Delta | Route size before → after |
|---|---|---|---|---|
/analytics |
589 kB | 106 kB | −483 kB (−82%) | 281 kB → 1.67 kB |
/kg/ecosystem |
251 kB | 175 kB | −76 kB (−30%) | 41.3 kB → 9.73 kB |
/kg/live |
193 kB | 150 kB | −43 kB (−22%) | 14.5 kB → 15.8 kB |
The heavy libraries did not disappear — they moved into async chunks fetched
when the component actually renders (with skeleton fallbacks in the page's own
loading idiom). /kg/live's per-route size rose 1.3 kB (the dynamic loader +
fallback markup) while its first load dropped 43 kB.
Shared chunks (all routes):
| Chunk | Before | After |
|---|---|---|
chunks/31255-*.js |
46 kB | 46 kB |
chunks/4bd1b696-*.js |
54.2 kB | 54.2 kB |
| other shared chunks | 2.72 kB | 3.6 kB |
| Total shared by all | 103 kB | 104 kB |
| Middleware | 92.6 kB | 92.6 kB |
4. Code-split inventory
Applied (next/dynamic at the importing page only; SSR left at its default in
all four cases — measured 2026-08-07, none of these components reads
window/document at module scope, so server-render semantics are unchanged):
| Import site | Component (heavy libs) | Fallback idiom |
|---|---|---|
app/analytics/page.tsx |
AdvancedAnalyticsDashboard (@tensorflow/tfjs, d3, recharts, framer-motion) |
root app/loading.tsx spinner on the dashboard's slate gradient |
app/kg/live/page.tsx |
GraphCanvas (d3) |
the page's own RefreshCw spinner over bg-gray-950/80 |
app/kg/ecosystem/page.tsx |
KGBrainVisualizer (d3, framer-motion) |
pulse skeleton on the component's bg-gray-900 rounded-xl footprint |
app/kg/ecosystem/page.tsx |
RoutingFlowDiagram (reactflow + stylesheet) |
same, h-[600px] |
app/kg/ecosystem/page.tsx |
CausalGraphExplorer (d3, framer-motion) |
same, h-[600px] |
CausalGraphExplorer sits in the same tab group as the two mapped ecosystem
targets and carries the same libs, so it was split in the same pass — the three
tabs now each load on first activation.
Skipped — already dynamic before this slice (measured):
| Site | Evidence |
|---|---|
app/boss-srpvdal/boss-srpvdal-client.tsx → BossAgentSRPVDAL |
dynamic(..., { ssr: false }) at line 6; route already 104 kB |
components/agent-ide/AgentIDE.tsx:32 and EnhancedAgentIDE.tsx:53 → MonacoEditor |
both parents already dynamic(...); /agent-ide 143 kB |
app/neural-brain/page.tsx:22 → NeuralBrainVisualization (three) |
already dynamic(...); route 145 kB |
Not split: PlanVisualization (also imports GraphCanvas) is imported only by
an archive-ui/ page outside the app build — no live route pays for it.
5. Budget rule (established by this slice)
- Command-center pages must not exceed their captured baseline First Load JS
(table below; family ceiling today: 125 kB). A PR that pushes any
/command-center/*route above its baseline needs an explicit, measured justification in the PR body. - The three changed routes must not regress above their after-numbers in §3 without the same justification.
- These budgets are measured local-build numbers (this doc); re-baseline only
with a fresh
npm run buildroute table committed alongside the change.
| Route | Baseline First Load JS |
|---|---|
/command-center |
125 kB |
/command-center/actions |
121 kB |
/command-center/approvals |
121 kB |
/command-center/audit |
121 kB |
/command-center/cells |
123 kB |
/command-center/decisions |
121 kB |
/command-center/domains |
113 kB |
/command-center/events |
122 kB |
/command-center/kg-live |
121 kB |
/command-center/learning |
109 kB |
/command-center/policies |
120 kB |
(Baseline column from the pre-change build; the after build read each +1 kB via the shared-chunk delta — the budget tracks the route's own weight, so ±1 kB shared-baseline movement does not count against a page.)
6. Fleet-grid virtualization — measured decision: NOT WARRANTED, SKIPPED
The work item named app/cells/page.tsx; that path does not exist in the
tree. The real 36-cell fleet surface is app/command-center/cells/page.tsx
(Phase 2C), which also sits outside this slice's writable zone. Measured render
counts of that page (2026-08-07):
- Static registry table: 36 rows (
FLEET_REGISTRY,FLEET_CELL_COUNT = 36). - Live health table: 36 rows + probes outside the transcription (bounded by the Boss probe fan-out; 0 in the static registry case).
- Illustrative certification walkthrough: 7 mock cards (
CELLSinlib/command-center/data.ts).
Worst case ≈ 79 rendered rows/cards — far under the ~200-row threshold at
which @tanstack/react-virtual pays for its complexity. Virtualization was
therefore not applied anywhere in this slice (honesty over checkbox): the
library remains installed and unused, ready for a genuinely long surface (for
example a future events/audit table that renders hundreds of rows in one
scroll container).
7. Remaining candidates (largest routes not yet split)
Baseline First Load JS, measured local build 2026-08-07. Attribution beyond the
direct imports noted below needs an ANALYZE=1 treemap — that is what §2 is for.
| Route | First Load JS | Direct heavy imports measured at the page file |
|---|---|---|
/boss/realtime |
262 kB | via components/graph/BossAgentPanel subtree (attribute with ANALYZE=1) |
/operations/email-intelligence |
261 kB | recharts imported directly by the page |
/operations/unified-revenue |
252 kB | recharts imported directly by the page |
/visualize/[kernelId] |
251 kB | via components/visualizations/* subtree |
/operations/cell32-optimizer |
231 kB | via component subtree |
/marketing/campaigns |
219 kB | via components/relu/CampaignLauncher subtree |
/marketing/conversions |
201 kB | — |
/marketing/audiences |
199 kB | — |
/relu |
196 kB | — |
/dashboard |
191 kB | — |
Note for the recharts pair: recharts is chart-per-page here, so the
/analytics pattern (dynamic at the page import site) applies directly.
8. Gates at completion of this slice
npx tsc --noEmit— exit 0.npm test— 83 files / 514 tests, all passed (matches the slice-start baseline).npm run build— exit 0 before and after the changes (lint + TS build gates enforced bynext.config.mjsremained on).
9. Slice 2 (2026-08-07, session 9) — the recharts pair split
Truth discipline: every number below is a measurement of a local
production build (npm run build, Next.js 15.5.21, standalone output,
2026-08-07). Nothing here is a live-infrastructure claim.
9.1 Method
- Baseline build: the session orchestrator's fresh full build of the untouched tree (exit 0; route table captured before any wave-1 edits).
- Changes applied: the §7
rechartspair only — each single-file'use client'page body moved verbatim into a colocated client component (email-intelligence-client.tsx,unified-revenue-client.tsx; sole edit = the exported function name) and eachpage.tsxbecame a thinnext/dynamicwrapper per the/analyticsprecedent. Zero content/copy changes to the rendered page bodies. - After build: one
npm run build(exit 0) in the shared working tree while two sibling work streams edited disjoint zones (lib/api/**+ proxy routes;lib/bff/adapters/boss.ts+app/api/bff/kg/*+app/command-center/kg-live/*) concurrently. - SSR semantics: left at the default in both wrappers — measured before
deciding: neither page body reads
window/documentat module scope (email-intelligence fetches only inside effects/handlers; unified-revenue's module scope reads onlyprocess.env, server-safe). - Fallback idiom (measured first, §4 discipline): email-intelligence has
its own loading early-return (
RefreshCw w-8 h-8 animate-spin text-blue-500 - "Loading Email Intelligence..." centered at
h-screenon the default surface) — the dynamic fallback mirrors it byte-for-byte plusrole="status". unified-revenue has no loading early-return, so its fallback composes the page's own measured idiom pieces: thecontainer mx-auto py-8 px-4 max-w-7xlshell, itsRefreshCw animate-spinin-flight motif, andtext-muted-foregroundsecondary text.
9.2 Noise floor (shared concurrent tree)
399 app routes in both tables — none added or removed. Of the 397 routes this
slice did not touch, 389 moved exactly 0 kB and 8 moved exactly +1 kB
(/blog, /blogs, /command-center/approvals, /email-agent,
/evaluations, /invest, /unauthorized, /vision) — a display-rounding
tick consistent with "other shared chunks" moving 3.6 → 3.64 kB while both
named shared chunks kept identical content hashes
(31255-c7c904d6581c47f1.js, 4bd1b696-bad92808725a934a.js). The floor is
therefore ±1 kB; the two changed-route deltas below are 147–156× that
floor.
9.3 Changed routes — before / after (measured local build, concurrent tree)
The after column is a real measurement of a completed exit-0 build (2026-08-07, ~20:29–20:35 UTC) taken while both sibling streams were still editing their disjoint zones; §9.2 quantifies exactly how quiet the rest of the table stayed. The wave-close quiet-tree confirmation build (§9.6) is the authoritative close-out number.
| Route | First Load JS before | First Load JS after | Delta | Route size before → after |
|---|---|---|---|---|
/operations/email-intelligence |
262 kB | 106 kB | −156 kB (−60%) | 15.4 kB → 2.12 kB |
/operations/unified-revenue |
253 kB | 106 kB | −147 kB (−58%) | 12.3 kB → 2.17 kB |
Shared chunks (all routes):
| Chunk | Before | After |
|---|---|---|
chunks/31255-c7c904d6581c47f1.js |
46 kB | 46 kB |
chunks/4bd1b696-bad92808725a934a.js |
54.2 kB | 54.2 kB |
| other shared chunks | 3.6 kB | 3.64 kB |
| Total shared by all | 104 kB | 104 kB |
| Middleware | 92.6 kB | 92.6 kB |
The recharts weight did not disappear — it moved into async chunks fetched when each dashboard renders. Budget rule (§5) extended: these two routes must not regress above their after-numbers without a measured justification.
9.4 Remaining candidates — §7 re-ranked (this slice's after build)
The §7 baselines drifted +1 kB on several routes between slice 1 and today's
builds (intervening session-8/9 merges); the numbers below are from this
slice's after build and supersede §7's ranking. The two recharts direct
importers are done; the rest need ANALYZE=1 attribution before splitting.
| Route | First Load JS | Attribution status |
|---|---|---|
/boss/realtime |
263 kB | via components/graph/BossAgentPanel subtree |
/visualize/[kernelId] |
252 kB | via components/visualizations/* subtree |
/operations/cell32-optimizer |
232 kB | via component subtree |
/marketing/campaigns |
220 kB | via components/relu/CampaignLauncher subtree |
/marketing/conversions |
202 kB | — |
/marketing/audiences |
200 kB | — |
/relu |
197 kB | — |
/dashboard |
192 kB | — |
/chat |
189 kB | — |
/simulation |
184 kB | — |
9.5 Gates at completion of this slice
npx tsc --noEmit— exit 0, run twice (pre-build and post-slice on the still-concurrent tree); zero errors in any zone both times.npm run build— exit 0 (lint + TS build gates enforced bynext.config.mjsremained on; compile warnings identical class to baseline — pre-existing OTel winston + exhaustive-deps warnings only).npm test -- --run(mid-edit shared tree): 90 files / 549 tests → 542 passed, 7 failed — every failure in sibling-zone files (app/command-center/kg-live/page.test.tsx5,app/api/boss-proxy/[...path]/route.test.ts2), zero failures touchingapp/operations/**or anything this slice imports. Wave-close quiet-tree rerun deferred to the orchestrator's go signal (§9.6).
9.6 Wave-close quiet-tree confirmation (PENDING)
Reserved for the single authoritative build after BOTH sibling streams land and the tree is quiet (orchestrator-signaled). Before numbers are the orchestrator's baseline log; §9.3's concurrent-tree measurements stand as measured but the cells below are the close-out record.
| Route | First Load JS before | First Load JS after (quiet tree) |
|---|---|---|
/operations/email-intelligence |
262 kB | 106 kB (route 2.12 kB) |
/operations/unified-revenue |
253 kB | 106 kB (route 2.17 kB) |
| Total shared by all | 104 kB | 104 kB |
Quiet-tree full-suite rerun: npm test -- --run exit 0 — 91 files /
575 tests, all passed (2026-08-07 wave close; build exit 0, 402 routes).
10. Cache policy (measured, 2026-08-07)
Claim labels: "measured (code)" = read from the tree; "measured (local
serve)" = observed on the wire from this slice's built standalone artifact
served at 127.0.0.1:3999 (E2E_SKIP_BUILD=1 scripts/e2e-prepare.sh, then
node .next/standalone/server.js). No live-infrastructure claims.
10.1 Client cache — React Query (measured, code)
The sole production QueryClient is lib/query-client.tsx, mounted app-wide
via app/layout.tsx → components/providers.tsx → QueryProvider
(app/journey-intelligence/page.tsx additionally re-mounts the same provider
nested — pre-existing, harmless, noted). Global defaults:
| Option | Value |
|---|---|
staleTime |
5 min |
gcTime |
10 min |
retry |
skip 4xx; otherwise up to 3 |
refetchOnWindowFocus |
false |
refetchOnReconnect |
true |
mutations retry |
1 |
Representative per-hook overrides (measured): hooks/api/useKgReads.ts
polls both KG summaries at refetchInterval 60 s with retry: 1;
hooks/api/useGovernanceReads.ts polls governance reads at 120 s default
with retry: 1; hooks/api/useBOSS.ts sets staleTime per surface from
10 s (queue-ish reads) to 120 s; hooks/agent-ide/useAgentIDE.ts uses
30–60 s staleTime + 60 s polls. Pattern: hooks override freshness per
surface; the 5-min global default is the quiet fallback.
10.2 Server emission — route code vs next.config (measured, code + local serve)
- Code: 28 of 253
app/api/**route files set aCache-Controlof their own — mostly SSE streams (no-cache[, no-transform]) plus two routes that declare cacheable values:app/api/kg/stats/route.ts(public, max-age=30, stale-while-revalidate=60; stale pathpublic, max-age=10) andapp/api/integrations/health/route.ts(public, max-age=60). 0 of the 31 BFF route files set anyCache-Control; each emitsNextResponse.jsoninline — there is no single central success emitter (only the error path sharesbffErrorPayload). - Config:
next.config.mjsheaders()sendsCache-Control: no-cache, no-store, must-revalidateon the/:path*catch-all (which matches/api/**) andpublic, max-age=31536000, immutableon/_next/static/:path*. - Wire (measured, local serve): pages
(
/operations/email-intelligence,/operations/unified-revenue), a BFF route (/api/bff/approvals/pending, 401 tenant_required),/api/kg/stats(403 upstream-mirror path), and/api/config/a2a-gateway(200) all emitted the config tripleno-cache, no-store, must-revalidate; the static chunk emittedpublic, max-age=31536000, immutable. The a2a-gateway data point is decisive: its only code path setsCache-Control: no-store, yet the wire carried the config value — the config catch-all overrides route-handler-set Cache-Control on the served response. Consequences (documented, not changed): kg/stats' and integrations/health'spublic, max-agenever reach the client today; SSE routes'no-transformis likewise overridden; and unit tests that assert route-set headers (e.g.app/api/config/a2a-gateway/route.test.tsexpectingno-store) pass at unit level while the wire differs. - Static assets: fingerprinted
/_next/staticimmutable-for-1-year is the correct posture and is confirmed on the wire.
10.3 Decision: DOCUMENT-ONLY (criteria measured)
An explicit Cache-Control: no-store at a central BFF emitter was NOT
implemented, because both instruction criteria fail: (1) no single central
point emits BFF successes — all 31 BFF routes call NextResponse.json
inline, so the change would touch >3 files, including the sibling-zone
app/api/bff/kg/*; (2) it would change zero wire bytes today — the
config catch-all already serves no-cache, no-store, must-revalidate on
every API/BFF response (measured, local serve).
Recommendation (recorded for a future slice): the config catch-all is
currently the single enforcement point for the no-store posture — do not
relax it without first revisiting the 28 route-level values (especially the
two public, max-age routes, which would silently become shared-cacheable —
a tenant-data risk once auth arms — and the SSE no-transform loss). When a
central BFF success emitter is eventually created (One Boss consolidation),
bake Cache-Control: no-store into it with a colocated test so the BFF
envelope's posture stops depending on the page-cache catch-all.