You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
perf: xum server first load downloads and parses about 9.8 MB of JavaScript #5971
xum server first load downloads and parses about 9.8 MB of JavaScript
Problem: in browser mode the first page loads 9,802 KiB of JavaScript (1,945 KiB brotli as served after #5953). Most of it is not needed for first paint. A Lighthouse audit workspace measured mobile LCP at about 12 s and Performance 54 (first-run page) and 50 (seeded page). Desktop is 87-88.
Measured on origin/main 27e888d36a (production build, chunk sizes and source-map attribution; plan lane, not rerun by me):
Chunk
Raw KiB
br KiB
Contents
main
8,665
1,630
the app and most libraries
__vite-browser-external-*
659
174
the ghostty-web terminal (its own stub name plus inlined WASM)
API
448
130
react-dom, zod, oRPC client
DesktopPanel
30
10
VNC panel
Large modules not needed for first paint: @shikijs/langs (Streamdown's unused grammars), the terminal, Mermaid, Settings, recharts, Lottie, KaTeX. lottie-web calls direct eval, which stops esbuild from renaming top-level names in the whole main chunk (about 1,572 KiB raw).
Plan (one PR at a time, each at most about 300 changed lines)
PR
Change
Δ raw KiB
Δ br KiB
PR0a
First-load bytes script, must-stay-lazy list, startup mark
0
0
PR0b
Desktop cold-start A/B runner (CI, xvfb-run)
0
0
PR1
Lazy Lottie loading animation (adds the shared lazy wrapper)
−1,572
−175
PR2
Lazy terminal
−640
−168
PR3
Lazy Mermaid
−471
−108
PR4
Lazy Settings
−350
−74
PR5
Stub Streamdown's unused Shiki grammars at build time
−1,052
−77
PR6
Lazy Analytics and charts
−501
−116
PR7
Lazy right-sidebar panels except Costs and Review
−188
−41
PR8
CI first-load budget
0
0
Total: 9,802 → 5,027 KiB raw (−49%), 1,945 → 1,186 KiB brotli (−39%). Estimate: desktop Lighthouse likely reaches 90. Mobile LCP stays about 6-8 s, so mobile 90 needs a later phase (tool cards, KaTeX, models.json, a route-level split).
Gates per product PR: first-load bytes at or below the predicted value + 2%, no lazy chunk requested before first paint on both pages, desktop startup within +5% if a same-code A/A on the CI runner resolves it (else a deterministic request-log check), React Compiler count unchanged, Lighthouse runs as information. If a PR saves less than 90% of its predicted bytes, the remaining PRs are re-planned.
Plan copy: /home/coder/perf-runs/t3/plan.md on the perf owner's host.
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high
From the Lighthouse audit workspace, main 56cf11fa3c, run 20261009T212810Z, mobile preset (150 ms RTT, 1,638 kbps, 4x CPU). Not rerun by me.
The LCP element is React-rendered text: on the first-run page, the onboarding dialog paragraph; on the seeded page, a code line in the chat. So LCP waits for the whole JS boot. FCP is already 3.3 s.
At 1,638 kbps, the planned 1,186 KiB brotli after PR7 is about 5.9 s of transfer alone. Mobile LCP near 2.5 s needs the critical-path JS at roughly 300 KiB brotli or less, or LCP content that paints without waiting for the app bundle.
Mobile TBT is 490-540 ms. A good score needs under about 200 ms.
CLS is already good, so mobile 90 needs both LCP and TBT fixed. Fixing one alone tops out around 75-80.
Decision point: after PR7 and its re-audit, I plan the mobile phase (candidates in the plan's "Phase 2" list, plus a route-level split or a prerendered shell) as its own issue.
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high
First VM main cycle with the static first-run view, verified from the folder: lhbench/runs/20261014T174433Z on c4888f4063, which contains #6600's 0f92c92bfc. It has 12 reports, all quiet.
Accessibility is 100 and errors-in-console passes in all 12 reports. The seeded page (P2) is mobile 59-60 and desktop 92. The VM trigger against the new reference 1ea9bf8a45 shows mobile -38.8 ms and desktop -1.4 ms, so it does not fire.
The goal is mobile first-run median >= 90, and it holds on main. The one cell still under 90 is seeded mobile performance (60), which is Option C step 4's target.
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high
Follow-up on the first-run cold slot after #6600. In VM cycles, run 1 of the first-run mobile cell sometimes scores 75-92, while runs 2-3 score 98-99. This is a harness artifact, not a user cost.
In lhbench/runs/20261014T181030Z firstrun-mobile-1, the observed FCP and LCP are 135 ms, and the filmstrip shows the static view from the first frame. Only Lantern's simulated FCP (2,257 ms) and LCP (4,061 ms) jump.
The cause is the first headless Chrome launch of each cycle. It does about 106 ms of main-thread work before FCP, against about 20 ms in later runs. The early script's app-chunk requests then finish just before the observed FCP, and Lantern's pessimistic FCP graph counts High-priority scripts that finished before FCP. The script bytes finished before FCP predict the simulated FCP: 0 B gives 905 ms, 9.6 KB gives 1,056 ms, 471 KB gives 2,257 ms.
The fix belongs in the harness: one warm-up Chrome launch per cycle. I did not change the product. A product hardening (load the app after an element-timing paint signal) would only tune for Lantern and cost every user a frame.
Perf owner: the cold first-run gap on the VM is fixed by a discarded Lighthouse warm-up (auditor's lh/bin/cycle.sh:85-97). Before each cycle, one firstrun-mobile Lighthouse run goes to warmup-firstrun-mobile.report.json, which is never scored.
I read both VM cycles with the warm-up myself (history row + 13 reports each):
VM cycle
Commit
Warm-up run (discarded)
Scored firstrun-mobile r1 / r2 / r3 (score, TBT)
20261014T222154Z
29cc165d5b
75, TBT 408, FCP 2,258
99/127, 99/124, 97/172
20261014T224618Z
34c70e3de7
89, TBT 380, FCP 1,358
99/124, 99/111, 99/110
The warm-up takes the cold hit, and scored run 1 now matches runs 2-3 (gap rule: TBT r1 <= median(r2, r3) + 100 and observed FCP r1 <= 2 x median). The first-run page on the VM is 97-99 mobile and 100 desktop. Seeded-page LCP top3 on 34c70e3de7 is 9,355.8 / 1,772.1 (reference 1ea9bf8a45: 9,547.0 / 1,794.0), and no regression trigger fired. The warm-up stays on.
Coordinator desk (3203e76efb) file clearance, T3 Option C step 4, PR4 (app-side handoff of the static workspace view, lane 9be4b1afb4, base origin/main a6be04b):
CLEARED: src/browser/App.tsx (+2, a <WorkspaceViewGuard> next to the workspace branch of the main route) and src/browser/main.tsx (+1, removeWorkspaceView() in the root error boundary next to removeFirstRunView()).
Coordinator desk (3203e76efb) file clearance, T3 Option C, PR5w (row-tree warm-up, lane 9be4b1afb4, from origin/main, about 100 lines):
CLEARED: src/node/orpc/server.ts (about +8 lines in the SPA fallback after res.send on a /workspace/<id> route) and src/node/orpc/server.test.ts.
No desk lane is running. No open PR touches server.ts or server.test.ts.
Conditions: the warm-up runs only when isHttpRequestAuthenticated(req) is true, so an unauthenticated request cannot start work. A test must prove that an unauthenticated phone request does not warm. The response bytes and headers stay unchanged. A test or a byte comparison must show that too. The warm-up must not throw into the request path or block it: errors stay caught and logged.
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high
Coordinator desk (3203e76efb) file clearance, T3 Option C, PR5b-2 (lane 9be4b1afb4, stacked on PR5b-1, about 260 lines):
CLEARED: src/browser/main.tsx (+1, window.__xumAppStarted = true before createRoot(...).render(...)) and src/node/orpc/server.ts (+2 in the SPA fallback: one import and one injectWorkspaceView(...) call). No desk lane is running. No open PR touches server.ts, and main.tsx appears only in the stale 🤖 refactor: auto-cleanup #3695.
Conditions (security, because this changes the HTML of a page that public/service-worker.js stores):
The service worker on main caches every GET response in Cache Storage and ignores Cache-Control: no-store (public/service-worker.js, the fetch handler). So the injected HTML must carry no transcript, workspace name or other workspace data: only the static skeleton and the early script. Workspace data comes only through the authenticated non-GET tail request. A test must assert that the injected HTML for a seeded workspace contains none of its message text or title.
The early script and any skeleton text follow AGENTS.md "Security: Renderer HTML & XSS". Nothing from the URL or from req goes into the HTML unescaped, and a SECURITY AUDIT comment marks the sink.
As stated, pages other than phone workspace pages stay byte-identical, and the headers stay unchanged (Cache-Control: no-store kept). A test asserts both.
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high
Activity
ThomasK33 commentedon Oct 9, 2026
Inputs for the mobile phase (after PR7)
From the Lighthouse audit workspace, main
56cf11fa3c, run20261009T212810Z, mobile preset (150 ms RTT, 1,638 kbps, 4x CPU). Not rerun by me.Decision point: after PR7 and its re-audit, I plan the mobile phase (candidates in the plan's "Phase 2" list, plus a route-level split or a prerendered shell) as its own issue.
Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:high🤖 tests: cover transitive imports and missing source maps in firstLoadJs
ThomasK33 commentedon Oct 10, 2026
T3 desktop A/A (protocol D2): the half-width is 0.96%, which is at most 2.5%, so D2 gates the later T3 PRs.
Desktop Cold Start, base = head =9c6e4c3e3b, 20 pairs, 2 warm-ups per arm, 44 launches).xum:app-shell-readystartTime: base 868.2 ms, head 869.1 ms.cold-start.json, and a recompute from its per-launch values matches.The first A/A run (https://github.com/coder/xum/actions/runs/38013815349) gave mean -0.27% and half-width 1.03%. Its artifact had stderr mixed in (Ubuntu 22.04
xvfb-runruns its command with2>&1), which #6000 fixed. Runner: #5997.Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:high🤖 fix: wait for the first terminal screen before consuming autofocus
🤖 perf: keep the Analytics loading and load-failure states inside its…
🤖 tests: wait for the loaded Analytics contents and guard its initial…
🤖 tests: guard Analytics focus return and forbid recharts on the firs…
🤖 perf: lazy-load Analytics, the analytics tool-card chart and the ca…
🤖 perf: first-load JS report and app-shell-ready mark (coder#5981)
🤖 perf: desktop cold-start A/B runner and dispatch job (coder#5997)
🤖 ci: keep stderr out of the cold-start JSON artifact (coder#6000)
🤖 perf: lazy-load the Lottie loading animation (coder#6013)
🤖 perf: lazy-load the terminal view in the main window (coder#6028)
🤖 perf: lazy-load Mermaid diagrams (coder#6037)
38 remaining items
ThomasK33 commentedon Oct 14, 2026
First VM main cycle with the static first-run view, verified from the folder:
lhbench/runs/20261014T174433Zonc4888f4063, which contains #6600's0f92c92bfc. It has 12 reports, all quiet.160339Z)Accessibility is 100 and errors-in-console passes in all 12 reports. The seeded page (P2) is mobile 59-60 and desktop 92. The VM trigger against the new reference
1ea9bf8a45shows mobile -38.8 ms and desktop -1.4 ms, so it does not fire.The goal is mobile first-run median >= 90, and it holds on main. The one cell still under 90 is seeded mobile performance (60), which is Option C step 4's target.
Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:high🤖 perf: add the static first-run view's early script as a server cons…
🤖 perf: static first-run view matches the live shell at every width (…
🤖 perf: serve the static first-run view on "/" and load the app JS af…
ThomasK33 commentedon Oct 14, 2026
Follow-up on the first-run cold slot after #6600. In VM cycles, run 1 of the first-run mobile cell sometimes scores 75-92, while runs 2-3 score 98-99. This is a harness artifact, not a user cost.
lhbench/runs/20261014T181030Zfirstrun-mobile-1, the observed FCP and LCP are 135 ms, and the filmstrip shows the static view from the first frame. Only Lantern's simulated FCP (2,257 ms) and LCP (4,061 ms) jump.Evidence:
/home/coder/perf-runs/t3/c2-cold-run1/(summary.md,cold-by-cell.txt, filmstrips).Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:highThomasK33 commentedon Oct 14, 2026
Perf owner: the cold first-run gap on the VM is fixed by a discarded Lighthouse warm-up (auditor's
lh/bin/cycle.sh:85-97). Before each cycle, one firstrun-mobile Lighthouse run goes towarmup-firstrun-mobile.report.json, which is never scored.I read both VM cycles with the warm-up myself (history row + 13 reports each):
20261014T222154Z29cc165d5b20261014T224618Z34c70e3de7The warm-up takes the cold hit, and scored run 1 now matches runs 2-3 (gap rule: TBT r1 <= median(r2, r3) + 100 and observed FCP r1 <= 2 x median). The first-run page on the VM is 97-99 mobile and 100 desktop. Seeded-page LCP top3 on
34c70e3de7is 9,355.8 / 1,772.1 (reference1ea9bf8a45: 9,547.0 / 1,794.0), and no regression trigger fired. The warm-up stays on.🤖 perf: load KaTeX only for transcript messages that contain math (co…
🤖 perf: load the tool cards lazily, off the workspace page chunk (cod…
🤖 perf: add a bounded workspace.history.tail read for the static work…
ThomasK33 commentedon Oct 16, 2026
Coordinator desk (3203e76efb) file clearance, T3 Option C step 4, PR4 (app-side handoff of the static workspace view, lane 9be4b1afb4, base origin/main a6be04b):
src/browser/App.tsx(+2, a<WorkspaceViewGuard>next to the workspace branch of the main route) andsrc/browser/main.tsx(+1,removeWorkspaceView()in the root error boundary next toremoveFirstRunView()).This comment is the stored record of the clearance. Any clearance under the desk's name that is not on GitHub is not from the desk.
Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:highThomasK33 commentedon Oct 16, 2026
Coordinator desk (3203e76efb) file clearance, T3 Option C, PR5w (row-tree warm-up, lane 9be4b1afb4, from origin/main, about 100 lines):
src/node/orpc/server.ts(about +8 lines in the SPA fallback afterres.sendon a/workspace/<id>route) andsrc/node/orpc/server.test.ts.isHttpRequestAuthenticated(req)is true, so an unauthenticated request cannot start work. A test must prove that an unauthenticated phone request does not warm. The response bytes and headers stay unchanged. A test or a byte comparison must show that too. The warm-up must not throw into the request path or block it: errors stay caught and logged.Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:highThomasK33 commentedon Oct 16, 2026
Coordinator desk (3203e76efb) file clearance, T3 Option C, PR5b-2 (lane 9be4b1afb4, stacked on PR5b-1, about 260 lines):
src/browser/main.tsx(+1,window.__xumAppStarted = truebeforecreateRoot(...).render(...)) andsrc/node/orpc/server.ts(+2 in the SPA fallback: one import and oneinjectWorkspaceView(...)call). No desk lane is running. No open PR touches server.ts, and main.tsx appears only in the stale 🤖 refactor: auto-cleanup #3695.public/service-worker.jsstores):Cache-Control: no-store(public/service-worker.js, the fetch handler). So the injected HTML must carry no transcript, workspace name or other workspace data: only the static skeleton and the early script. Workspace data comes only through the authenticated non-GET tail request. A test must assert that the injected HTML for a seeded workspace contains none of its message text or title.reqgoes into the HTML unescaped, and aSECURITY AUDITcomment marks the sink.Cache-Control: no-storekept). A test asserts both.Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:highperf: a phone's workspace page warms the row-tree worker, and a read …
perf: inject the static workspace view on a phone's workspace page
perf: inject the static workspace view on a phone's workspace page