Summary
Switching between pinned session tabs is very slow (UI frozen ~5+ seconds when switching to a long session). The network is not the bottleneck — the cost is synchronous re-rendering of the entire conversation, done 2–3× per switch.
How it was measured
Pinned 4 long sessions (1453, 1315, 1069, 808 messages) and instrumented tab switches in the browser with:
- a wrapped
fetch for per-endpoint timing,
- a
MutationObserver on the messages container to detect render settle,
- a
longtask PerformanceObserver to quantify main-thread blocking.
Results — switching to the 877-node "Ui" session
- 5,564 ms total main-thread blocking, dominated by two ~2,580 ms long tasks
firstRenderMs ≈ 2,600–3,000 ms; total settle ≈ 5,750 ms
/api/messages returned in 38 ms, /api/sessions/open 6–290 ms, /api/state ~80 ms
The two back-to-back ~2.58 s long tasks are two full re-renders of the same conversation.
Root causes (in order of impact)
1. Entire conversation is re-rendered synchronously on every switch — and twice
The double render happens because a switch triggers refreshMessages() down two independent paths:
openSessionTab() → refreshState() → refreshMessages() (src/sessions/sessionDrawer.ts:475, src/main.ts refreshState)
- The server's
/api/sessions/open handler calls broadcast({ type: "state_changed" }) (server.ts:1805), and the realtime client responds with another refreshMessages() (src/realtime/realtime.ts:288-289)
Each refreshMessages does clearInternal() + rebuild of hundreds of DOM nodes + markdown parsing, with no virtualization, no diffing, and no per-session render cache (src/messages/messageList.ts:512). Logs showed 2–3 /api/messages calls per switch.
2. "Slow" artifact fetches are a symptom, not a cause
Logs showed /api/artifacts/*.md taking ~2,300–2,500 ms each, but curling the server directly returns them in 1.5 ms (404 — the file belongs to a different session's cwd). The multi-second timing is purely because the await/.then() can't resume until the giant synchronous render finishes; the fetches are queued behind a blocked main thread. Two secondary issues feed this:
enhanceArtifactLinks (src/markdown/render.ts:213) re-fetches every artifact preview on every render and caches nothing; each duplicated render wave re-fires them.
- Artifacts are resolved against the current server cwd, not the session's cwd, so cross-cwd sessions always 404.
Impact
Per switch to a long session the UI freezes ~5+ seconds, roughly half of which is purely redundant (the second full render from the websocket broadcast).
Suggested fixes (not yet applied)
- Kill the double render — dedupe
refreshState's refreshMessages against the state_changed broadcast the same client just caused (ignore self-triggered open broadcast, or coalesce refreshes). Roughly halves switch time on its own.
- Stop full re-rendering — cache rendered conversation DOM per session and swap it in, or virtualize/window the message list and render markdown lazily (an IntersectionObserver-based lazy renderer exists in
render.ts, but the initial build is still eager).
- Cache artifact previews and resolve them against the session's cwd so they don't 404 / re-fetch on every render.
Summary
Switching between pinned session tabs is very slow (UI frozen ~5+ seconds when switching to a long session). The network is not the bottleneck — the cost is synchronous re-rendering of the entire conversation, done 2–3× per switch.
How it was measured
Pinned 4 long sessions (1453, 1315, 1069, 808 messages) and instrumented tab switches in the browser with:
fetchfor per-endpoint timing,MutationObserveron the messages container to detect render settle,longtaskPerformanceObserverto quantify main-thread blocking.Results — switching to the 877-node "Ui" session
firstRenderMs≈ 2,600–3,000 ms; total settle ≈ 5,750 ms/api/messagesreturned in 38 ms,/api/sessions/open6–290 ms,/api/state~80 msThe two back-to-back ~2.58 s long tasks are two full re-renders of the same conversation.
Root causes (in order of impact)
1. Entire conversation is re-rendered synchronously on every switch — and twice
The double render happens because a switch triggers
refreshMessages()down two independent paths:openSessionTab()→refreshState()→refreshMessages()(src/sessions/sessionDrawer.ts:475,src/main.tsrefreshState)/api/sessions/openhandler callsbroadcast({ type: "state_changed" })(server.ts:1805), and the realtime client responds with anotherrefreshMessages()(src/realtime/realtime.ts:288-289)Each
refreshMessagesdoesclearInternal()+ rebuild of hundreds of DOM nodes + markdown parsing, with no virtualization, no diffing, and no per-session render cache (src/messages/messageList.ts:512). Logs showed 2–3/api/messagescalls per switch.2. "Slow" artifact fetches are a symptom, not a cause
Logs showed
/api/artifacts/*.mdtaking ~2,300–2,500 ms each, but curling the server directly returns them in 1.5 ms (404 — the file belongs to a different session's cwd). The multi-second timing is purely because theawait/.then()can't resume until the giant synchronous render finishes; the fetches are queued behind a blocked main thread. Two secondary issues feed this:enhanceArtifactLinks(src/markdown/render.ts:213) re-fetches every artifact preview on every render and caches nothing; each duplicated render wave re-fires them.Impact
Per switch to a long session the UI freezes ~5+ seconds, roughly half of which is purely redundant (the second full render from the websocket broadcast).
Suggested fixes (not yet applied)
refreshState'srefreshMessagesagainst thestate_changedbroadcast the same client just caused (ignore self-triggered open broadcast, or coalesce refreshes). Roughly halves switch time on its own.render.ts, but the initial build is still eager).