Repository navigation
perf(desktop): switching back to a long session mounts the whole transcript in one commit, freezing the UI ~0.35s #2052
Description
Problem
Switching back to a long session freezes the renderer for ~0.35s. The freeze is a single long frame, reproducible on every switch — not a statistical tail.
Measured on a real 30-turn session (22196 DOM nodes, 51610px scroll height), 120Hz display, Electron main build at 4887c99.
| Action | p95 | max frame | frames >50ms |
|---|---|---|---|
| Switch back to the long session | 9.3ms | 333–392ms | 1–3 |
| Switch away to a 2-turn session (4627 nodes) | 8.9ms | 17–25ms | 0 |
| Scroll the long transcript (18 runs) | 16.8–24.6ms | 41.7ms | 0 |
| Open/close settings, collapse/expand sidebar | 8.7–9.0ms | 9.3ms | 0 |
Five consecutive switch-back runs: 341.6 / 366.7 / 350.1 / 341.7 / 333.3 ms. Every other renderer path measured is clean, and steady-state streaming (#2030, fixed by #2034) is clean too — this is the only path that still drops a user-visible frame.
Attribution
devtools.timeline trace of the freeze frame, three items summing to the observed cost:
FunctionCall 147.0ms React rendering the new transcript tree
UpdateLayoutTree 57.2ms 15024 elements restyled
Layout 144.4ms dirty 16399 / total 21180 objects
--------
348.6ms ≈ observed 333–392ms
The style and layout items carry no JS stack — the trace attributes them to the rendering pipeline, not to a forced synchronous layout. Cumulative style recalc over the switch touches 98641 elements against ~22196 real nodes, so every element is restyled ~4.4 times, with 33 recalcs over 1000 elements accounting for 90% of the total.
The cost tracks transcript size rather than the switch itself: the same code path into a 2-turn session spends 0.020s in style recalc versus 0.435s here.
Ruled out
Two plausible causes were tested and are not responsible. Recording them so they don't get re-investigated:
- The Astryx spring scroll animation.
useChatStreamScroll's rAF spring readsscrollHeight/clientHeight/scrollTopand writesscrollTopevery frame, causing 98 forced synchronous layouts (CPU profile:get scrollTop89.9ms,get scrollWidth41.1ms,get scrollHeight14.5ms). Unlocking the scroll so the spring never runs cuts layout count 139 → 19 and style recalc 0.44s → 0.146s, but the freeze frame is unchanged (341.6 / 358.7ms). - The
content-visibilityturn-size warm-up (turn-size-warmup.ts, added for fix(ui): long-session scroll geometry — endless upward scroll, and remount landing mid-document #827). Disabling the warm-up walk behind a runtime flag leaves the freeze unchanged (366.7 / 333.4ms), and re-enabling it reproduces the baseline.
Direction
The freeze is the intrinsic cost of inserting ~22k nodes in one commit, so the fix has to reduce what gets mounted rather than what runs afterwards:
- mount only the turns near the viewport and fill the rest during idle time, or
- keep a switched-away transcript's DOM mounted and hidden, trading memory for the remount.
Both are architectural; neither is a local patch.
Also observed (not part of this issue)
The spring animation's 98 forced synchronous layouts and ~0.3s of extra style recalc are real waste, but they cause no perceptible jank (p95 stays at 9ms) and have no clean fix seam. ChatLayout exposes no way to follow instantly instead of springing, and re-keying it would unmount the Composer, which by contract stays mounted to preserve per-session unsent drafts (#646, #1044). Cheaper to revisit once the mounted node count drops.
References
- fix(ui): long-session scroll geometry — endless upward scroll, and remount landing mid-document #827 — long-session scroll geometry, same
content-visibilitymachinery (closed) - refactor(ui): stream the transcript incrementally instead of re-deriving it per token #2030 / refactor(ui): stream the transcript incrementally instead of re-deriving it per token #2034 — incremental streaming, the previously fixed path
Activity
Benjamin-eecs commentedon Aug 4, 2026
Taking this one, along the issue's first direction: on switch, mount only the last few turns, then fill upward in idle chunks with scroll compensation. The full turns array still feeds presentation caching and the prompt rail; search navigation into an unmounted turn force-completes the window first. Announced in the group earlier today.
perf(ui): mount a switched-to transcript progressively (apache#2191)
fix(ui): land a switched-to session at its latest turn instead of fly…