Skip to content

perf(desktop): switching back to a long session mounts the whole transcript in one commit, freezing the UI ~0.35s #2052

Closed

Description

@Astro-Han

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 reads scrollHeight/clientHeight/scrollTop and writes scrollTop every frame, causing 98 forced synchronous layouts (CPU profile: get scrollTop 89.9ms, get scrollWidth 41.1ms, get scrollHeight 14.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-visibility turn-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

Activity

  1. Benjamin-eecs commented on Aug 4, 2026

    @Benjamin-eecs
    Contributor

    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.

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