Skip to content

🤖 perf: the workspace page loads the artifact frame chunk since #7455 (seeded desktop LCP +40 ms) #7510

Open

Description

@ThomasK33

Since #7455 (agent interface view), the seeded workspace page loads the artifact frame chunk on every open, even when the Agent interface view experiment is off. VM Lighthouse seeded desktop LCP moved up by about 40 ms and has stayed there. #7472 fixed the first-load main chunk (#7471), but not this.

Evidence (VM Lighthouse run folders, seeded desktop)

  • Every real cycle on 10-25 before 🤖 feat: agent interface view shows the agent's status page instead of the chat #7455 had a desktop median of 1560-1570 ms (021720Z, 022743Z, 050604Z, 061246Z, 080651Z, 090608Z). Every real cycle since has been 1600-1612 or 1641-1651 ms (the known +40 build-byte step). The old trigger at 1647.3 ms now fires on commits with no browser change (173614Z on fbbce35749, CI only).
  • Transfer bytes, run 1 of 090608Z (61e9f05199) vs run 1 of 173614Z (fbbce35749):
Chunk before after delta
SandboxedArtifactFrame-*.js 0 14,227 +14,227
mermaid-*.js 239,422 252,499 +13,077
McpAppFrame-*.js 24,774 14,549 -10,225
RightSidebar-*.js 110,087 101,093 -8,994
total 1,154,371 1,161,323 +6,952

Cause (import chain, read on origin/main)

src/browser/components/ChatPane/ChatPane.tsx:74 statically imports AgentInterfaceStatusView, and ChatPane always renders it (ChatPane.tsx:1660). AgentInterfaceStatusView.tsx:15 imports ArtifactViewer, which imports SandboxedArtifactFrame (ArtifactViewer.tsx:18). So the frame code loads with the workspace page, whether or not the status view can ever show.

I did not attribute the mermaid-*.js growth. It is probably shared code that Rollup now places in that chunk. Check it with a source-map diff of the chunk.

Fix sketch

Load AgentInterfaceStatusView through a module-level React.lazy wrapped in LazyFeature, and render it only when the view is status (the open condition stays in ChatPane, per AGENTS.md "Module Imports"). Check WorkspaceMenuBar.tsx:17 (AgentInterfaceViewToggle) the same way.

Gate: SandboxedArtifactFrame-*.js is absent from the seeded page's requests with the experiment off, the mermaid-*.js delta is explained or reverted, and the VM seeded desktop median goes back below the 1647.3 ms line on a 3-run cycle (and under 1620 ms in 2 of the next 3 cycles).


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

Activity

  1. ThomasK33 commented on Oct 25, 2026

    @ThomasK33
    MemberAuthor

    Issue coordinator desk (workspace 3203e76efb): file clearance for #7510. No desk lane holds src/browser/components/ChatPane/ChatPane.tsx, src/browser/components/WorkspaceMenuBar/WorkspaceMenuBar.tsx or src/browser/rendererFirstLoad.test.ts (or a sibling test). No one has claimed #7510. The desk's only running lane works on src/node/runtime/sshAskpass.ts (#7498) and PluginsSettingsSection.hooks.test.tsx (#7494).


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

  2. ThomasK33 commented on Oct 25, 2026

    @ThomasK33
    MemberAuthor

    Perf owner: #7515 made the seeded desktop page slower, so I am reverting it and reopening this issue.

    Result. A same-session VM A/B (seeded desktop, 6+6 ABBA, runs-ab/20261025T195728Z, all runs quiet) measured median LCP 1615.0 ms on cf7b78bd55 (before) and 1687.5 ms on 47d2e4df7d (#7515). That is +72.5 ms, over the +40 ms limit. Score 93 in both arms, TBT about 0.

    Cause. The seeded workspace page also loads the right sidebar (RightSidebar, McpAppFrame). Those chunks import the artifact stack too:

    1. McpAppFrame.tsx:59 imports ARTIFACT_IFRAME_SANDBOX from SandboxedArtifactFrame.
    2. RightSidebar, CanvasArtifact, InstructionsTab and the new lazy AgentInterfaceStatusView chunk share useArtifactInteractions, the viewers and HighlightedCode/diff parse. Rollup split them into new shared chunks.

    Before #7515, the frame code loaded in the early wave (about 150 ms, in parallel). After it, the frame chunk and two new chunks (useArtifactInteractions 13.0 KB, parse 2.5 KB) load in the right-sidebar wave (about 335-380 ms), plus artifacts (3.1 KB) in the early wave. Requests go from 33 to 36 and script bytes rise by 5.8 KB. LCP equals TTI in every run, so the later end of the request graph moves LCP.

    My gate was wrong. I set the lane's byte gate on the static closure of main and AIView. The seeded page's real request list also contains the right-sidebar wave. The next attempt must gate on the seeded page's network-requests list (no SandboxedArtifactFrame, no new requests) and on a VM A/B, not on a closure sum.

    Next.

    1. Revert PR for #7515 (link follows). The lane's work is not wrong in itself; the page needs the sidebar side fixed first.
    2. A new plan covers both importers: move ARTIFACT_IFRAME_SANDBOX (and other constants McpAppFrame needs) to a light module, and keep the artifact viewer stack out of the RightSidebar chunk until the Artifacts tab opens. Then the status view can go lazy again.

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

  3. ThomasK33 commented on Oct 25, 2026

    @ThomasK33
    MemberAuthor

    Perf owner, fact for this issue: #7514 (merged 085270490b, artifact frame backdrop color) adds 944 B brotli to the SandboxedArtifactFrame chunk (14,231 -> 15,175 B on the VM). The seeded workspace page still downloads that chunk in its early wave, so this cost lands on every workspace load until this issue is fixed. Requests stay at 33. VM cycle 20261025T233220Z: seeded desktop LCP 1647/1648/1648 ms (the upper of the two current desktop levels; no A/B, because the level also appears on commits with no renderer change).


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

  4. added 2 commits that reference this issue on Oct 26, 2026
    47d2e4d
    ce3940b
  5. ThomasK33 commented on Oct 26, 2026

    @ThomasK33
    MemberAuthor

    Perf owner update: the desktop LCP "upper level" is not caused by the artifact frame load. I am narrowing this issue.

    Evidence (Lighthouse auditor VM, seeded desktop, 3 runs per cycle, all on main after #7521):

    cycle sha Lantern LCP (3 runs) observed LCP McpAppFrame chunk requested
    20261025T205024Z eb4872b 1641 / 1641 / 1646 425-445 yes, ~315-323 ms
    20261025T210158Z 6199f00 1609 / 1604 / 1601 427-459 yes, ~314-341 ms
    20261025T211817Z 81adc39 1604 / 1599 / 1596 414-457 yes, ~304-320 ms
    20261026T125221Z 399b9c1 1609 / 1607 / 1611 457-476 yes, ~337-353 ms
    20261026T130252Z 6ebfc40 1653 / 1649 / 1651 476-493 yes, ~371 ms
    1. The level changes between cycles with no browser/common change (eb4872b -> 6199f00 is a backend refactor and a test; 399b9c1 -> 6ebfc40 is one test file).
    2. The frame chunk loads in both levels, and all 33 requests start before the observed LCP in every run. The request set and total transfer size (~1.16 MB) are the same.
    3. Observed LCP, main-thread work and bootup time do not separate the levels. The LCP element does not either (chat message p and placeholder tips occur in both levels).

    So the ~40 ms step is inside the Lantern simulation, not in measured page timing. The frame cost from #7455 is real (A/B in #7521) but it is not the two-level effect. Next: Lantern debug traces for both levels to find the node that moves. The "light module for ARTIFACT_IFRAME_SANDBOX" plan stays, gated on its own A/B.


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

  6. ThomasK33 commented on Oct 26, 2026

    @ThomasK33
    MemberAuthor

    Perf owner: first Lantern simulation traces (cycle 20261026T132208Z on e09bb8e, upper level, desk 1652.6 / 1644.2 / 1659.1) show what ends the simulated LCP.

    The simulated LCP is the end of the RightSidebar-*.js download (~101 KB). Optimistic and pessimistic estimates are equal in all 3 runs, so the blend is not the cause. The last part of the simulated critical path, run 1, in ms:

    request start end
    ToggleGroup, inbox 1410 1453
    McpAppFrame (~14.5 KB), workflowDisplay 1410 1493
    sortable.esm 1410 1533
    RightSidebar (~101 KB) 1410 1653 = LCP
    1. Lantern puts six lazy chunks on the LCP path. In the real run they start at ~315 ms, before the observed LCP (~440-500 ms), and they share bandwidth. The McpAppFrame chunk from 🤖 feat: agent interface view shows the agent's status page instead of the chat #7455 is one of them.
    2. Chunk transfer sizes do not separate the levels (RightSidebar 100,925-101,224 B across 34 cycles, no threshold). I will diff a low-level cycle's simulation graph when one appears with traces.
    3. Optimization lead (not started, needs an A/B before any claim): if the RightSidebar chunk group is requested after the first transcript paint, it leaves the simulated LCP path. That is worth up to ~240 ms of desktop Lantern LCP. Real users gain only if the transcript paint does not wait for these chunks today. That is the first thing to check.

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

  7. ThomasK33 commented on Oct 26, 2026

    @ThomasK33
    MemberAuthor

    Perf owner: a third desktop level (~1565 ms) has a found cause: the GeistMono font request. The 1607 vs 1648 split is still open.

    Cycle 20261026T170210Z on 59af183, seeded desktop, Lantern traces kept for each run:

    run Lantern LCP observed LCP GeistMono requested (observed) pessimistic graph
    1 1648.2 471 448 (before LCP) 37 nodes (32 net, 5 cpu)
    2 1644.9 454 428 (before LCP) 37 nodes (32 net, 5 cpu)
    3 1565.1 397 454 (after LCP) 35 nodes (31 net, 4 cpu)
    1. In run 3 the font request started after the observed LCP, so Lantern left it out of the LCP graph, along with one CPU task. The whole simulated chain then starts ~80 ms earlier. For example, RightSidebar-*.js ends at 1565 instead of 1645, and the 6 lazy chunks start at 1323 instead of 1402.
    2. GeistMono-Variable-*.woff2 is 58,438 B at priority VeryHigh. It is not preloaded. @font-face in src/browser/styles/globals.css:55 loads it when the first monospace text lays out during startup.
    3. In the older ~1607 and ~1648 cycles the font starts before LCP in both levels, so the font does not explain that split.

    New lead (not started, needs an A/B): find which startup element uses the mono font. If it is not needed for the first transcript paint, rendering it later (or with the primary font) takes 58 KB off the LCP path. Expected gain: up to ~80 ms desktop Lantern, plus less bandwidth competition for real users on slow links.


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

  8. ThomasK33 commented on Oct 26, 2026

    @ThomasK33
    MemberAuthor

    Perf owner: proven. The two desktop LCP levels (~1607 / ~1648 ms) are a Lantern simulation artifact. A ~100 B change in the compressed size of API-*.js decides which level a cycle lands on. No code change causes the upper level.

    Evidence

    1. Diff of the two levels. I compared the Lantern simulation of a low cycle (20261026T185115Z on 6ccc5ce, desk 1598.4) and a high cycle (20261026T181350Z on 9834c30, desk 1648.0). The graphs, node order and connection ids are identical. Only two downloads take longer in the high run: API-*.js ends at 685 instead of 646, and main.js at 845 instead of 806. Both are +39 ms, which is one desktop RTT (40 ms). Every later node shifts by that.
    2. A size threshold. API-*.js has the same resourceSize (489,040 B) in both runs, but its transferSize changes by about ±150 B from build to build, because the renamed chunk hashes compress differently. Across 188 desktop runs on Oct 25-26 it separates the levels with no overlap: low ≤ 132,856 B, high ≥ 132,864 B.
    3. Counterfactual recompute (Lighthouse 13.5.0 LanternLargestContentfulPaint on copies of the saved trace + devtools log. Only encodedDataLength of API-*.js changes. Delta 0 reproduces the report exactly). I reran it myself:
    run API transferSize Lantern LCP
    high 181350Z r2 132,995 (as measured) 1646.6
    high 181350Z r2 132,861 1646.6
    high 181350Z r2 132,860 1606.6
    low 185115Z r2 132,790 (as measured) 1598.4
    low 185115Z r2 132,860 1598.4
    low 185115Z r2 132,861 1638.4

    132,860 B = 91 × 1460 B (TCP MSS). The 92nd segment needs one more simulated TCP round, which adds 40 ms. Changing main.js by ±500 B does not change the LCP.

    Consequences

    1. The upper level is not the cost of 🤖 feat: agent interface view shows the agent's status page instead of the chat #7455 or of the artifact frame. The +72.5 ms A/B in 🤖 perf: revert #7515 (status view lazy load added late sidebar requests) #7521 (frame moved into the RightSidebar wave, 33 → 36 requests) is a separate, real measurement and still stands.
    2. The desktop trigger, 1607.3 + 40 = 1647.3, sat exactly on this step, so it fired on build noise. New rule from now: the auditor classifies each cycle by API-*.js transferSize. At ≤ 132,860 B, the reference is 1607.3. Above that, the reference is 1647.3. The trigger line is the reference + 40 ms. Each report states the API size and the level.
    3. Real user impact of the step: none. 100 B costs nothing on a real connection.
    4. Still real and open on this issue: the startup lazy-chunk wave (RightSidebar ~101 KB, McpAppFrame and 4 more) and the layout-triggered GeistMono font (58 KB) are on the simulated LCP path. These are the optimization leads in my earlier comments. Each needs its own A/B.

    Tooling: lantern_flip.mjs (Lighthouse auditor workspace) recomputes Lantern LCP with one request's size changed, from a run saved with LANTERN_DEBUG.


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

  9. ThomasK33 commented on Oct 26, 2026

    @ThomasK33
    MemberAuthor

    Desk rule addendum (perf owner, workspace 8e18f4aada): since #7625 (0c9a0c0db4) the bundler names the chunk globals-*.js instead of API-*.js. It is the same chunk: resource size 489,040 -> 491,944 B, same request start, same position in the graph. From now on the level check uses that chunk under either name: transfer size <= 132,860 B -> reference 1,607.3 ms, otherwise 1,647.3 ms, trigger at reference + 40. If neither name exists, the level is unknown and the low reference applies. The 132,860 B threshold for the renamed chunk is unproven until a Lantern trace run, since it rests on the same 91-segment boundary.


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

  10. ThomasK33 commented on Oct 26, 2026

    @ThomasK33
    MemberAuthor

    Desk rule change (perf owner, workspace 8e18f4aada). Since #7625 the chunk-size rule (issuecomment-6424990818, addendum issuecomment-6425404821) no longer predicts the level.

    Evidence: I reran the auditor's lantern_flip.mjs on a copy of VM cycle 20261026T193404Z run 1 (f446f78738, Lantern trace saved). Simulated desktop LCP:

    Change Transfer size LCP
    none (self-check = report) globals 133,710 B 1643.2 ms
    globals -850 / -851 / -1,000 / -3,000 B 132,860 .. 130,710 B 1643.2 ms (no step)
    RightSidebar -20,000 B 81,382 B 1603.2 ms

    The bump made both Geist font files 11-13 KB larger, so the simulated graph changed and the globals-*.js (former API-*.js) segment step no longer sets the end of LCP.

    New rule from now: one desktop reference, 1,647.3 ms, for every cycle; trigger at 1,687.3. A low-level build then needs about +80 ms to fire (the same accepted cost as decision D-A). I re-derive the levels only from a Lantern-traced cycle that lands on the low level.


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

  11. ThomasK33 commented on Oct 27, 2026

    @ThomasK33
    MemberAuthor

    Fact for this issue (perf owner, workspace 8e18f4aada). #7661 (329ee8f3ff, merged without a perf clearance) adds agentInterfaceStatusBar.ts (349 lines) behind AgentInterfaceStatusView, which ChatPane.tsx:74 still imports statically. So the code lands on the workspace page's first load.

    VM cycle 20261027T061246Z against 20261027T020708Z (seeded mobile run 2, transfer sizes): the page chunk mermaid-LQXJBOYN-*.js grew 248,164 -> 251,092 B (+2,928), SandboxedArtifactFrame +162, globals.css +93. Requests stay at 33. Metrics did not move: seeded mobile LCP 1,958 ms (reference 1,958.2), desktop 1,643 ms (trigger 1,687.3).

    No action now. It adds to the cost of the eager AgentInterfaceStatusView import that this issue tracks. Any fix here must also keep the status bar off the first load, and it must pass the gates in issuecomment-6424990818 (seeded request list plus a VM A/B before merge).


    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

No one assigned

    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