Skip to content

perf: xum server first load downloads and parses about 9.8 MB of JavaScript #5971

Open

Description

@ThomasK33

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

Activity

  1. self-assigned this
    on Oct 9, 2026
  2. ThomasK33 commented on Oct 9, 2026

    @ThomasK33
    MemberAuthor

    Inputs for the mobile phase (after PR7)

    From the Lighthouse audit workspace, main 56cf11fa3c, run 20261009T212810Z, mobile preset (150 ms RTT, 1,638 kbps, 4x CPU). Not rerun by me.

    1. 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.
    2. 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.
    3. Mobile TBT is 490-540 ms. A good score needs under about 200 ms.
    4. 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

  3. ThomasK33 commented on Oct 10, 2026

    @ThomasK33
    MemberAuthor

    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.

    • Run: https://github.com/coder/xum/actions/runs/38015899054 (Desktop Cold Start, base = head = 9c6e4c3e3b, 20 pairs, 2 warm-ups per arm, 44 launches).
    • Mean per-pair delta -0.04%, one-sided 95% half-width 0.96%, upper bound 0.92%. The tolerance is +5%.
    • Median xum:app-shell-ready startTime: base 868.2 ms, head 869.1 ms.
    • The values come from the uploaded 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-run runs its command with 2>&1), which #6000 fixed. Runner: #5997.


    Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

  4. 38 remaining items

  5. ThomasK33 commented on Oct 14, 2026

    @ThomasK33
    MemberAuthor

    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.

    First-run page Run 1 Run 2 Run 3 Before #6600 (VM 160339Z)
    Mobile perf 92 98 97 (median 97) 76
    Mobile LCP (ms) 1,656 1,505 1,806 4,054
    Mobile FCP (ms) 906 905 1,056 —
    Desktop perf 100 100 100 99

    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

  6. ThomasK33 commented on Oct 14, 2026

    @ThomasK33
    MemberAuthor

    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.
    • Across 42 cycles, the median run-1 TBT is 356 ms against 180/183 ms for runs 2-3. That gap existed before 🤖 perf: serve the static first-run view on "/" and load the app JS after it paints (T3 C step 2, PR 4b) #6600 too. The seeded run 1 on an equally fresh server is normal, so the server is not the cause.
    • 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.

    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: high

  7. ThomasK33 commented on Oct 14, 2026

    @ThomasK33
    MemberAuthor

    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.

  8. ThomasK33 commented on Oct 16, 2026

    @ThomasK33
    MemberAuthor

    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):

    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: high

  9. ThomasK33 commented on Oct 16, 2026

    @ThomasK33
    MemberAuthor

    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

  10. ThomasK33 commented on Oct 16, 2026

    @ThomasK33
    MemberAuthor

    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):
      1. 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.
      2. 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.
      3. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions