Skip to content

Tab switching is slow: conversation fully re-rendered 2-3x per switch (~5s main-thread block on long sessions) #10

@ashwin-pc

Description

@ashwin-pc

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)

  1. 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.
  2. 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).
  3. Cache artifact previews and resolve them against the session's cwd so they don't 404 / re-fetch on every render.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions