Skip to content

client.interaction: scrolling up a long chat timeline saturates the main thread #103

Closed

Description

@mubeda

Summary

Scrolling up toward the top of a long agent conversation in the macOS desktop app lags badly. For about 12 minutes of scrolling, the Profiler recorded the web main thread saturated: 1.2 to 2.7 s of stalls in every 10 s, interactions up to 736 ms to paint, and RPC replies that the server answered in 13 ms or less waiting up to 900 ms in the client. The server and desktop host stayed idle. Code reading points at the per-row mount cost of the virtualized timeline: markdown re-parsed with rehype-raw on every remount, and synchronous Shiki highlighting of code blocks seen for the first time. This is not yet confirmed with a performance trace.

Environment

Field Value
BiBCode 0.9.2, desktop (DMG)
OS macOS 27.0.1, arm64
Mode local
Finding client.interaction-1bb57fd93dcf: warn, seen 4 times from 2026-10-14T16:12:17Z to 2026-10-14T16:31:22Z
Fingerprint 1bb57fd93dcf
Capture native dumps available, OS crash records available

Related findings from the same window: client.stall-93202704f780 (also #99, but these occurrences are real, not throttling), rpc.slow-37b7a238df64 and rpc.slow-ee0e8d26aa17 (server.getProcessDiagnostics), and rpc.slow-2cb2caf8ee75 (previewAutomation.focusHost). All three rpc.slow findings have slow part network or render, with server time under 13 ms.

Evidence

  • 18910b78-1326 to 18910b78-1465 client.batch, 16:12Z to 16:24Z, window focused, with flush windows on time (about 10.0 to 10.5 s, so not timer throttling): 7 to 15 stalls of 200 to 930 ms in each batch (for example 18910b78-1384: 12 stalls, 2,258 ms total, max 930 ms; -1459: 2,675 ms total).
  • 18910b78-1326, -1425, -1527 client.batch: Event Timing interactions of 672, 528 and 736 ms.
  • 18910b78-1423 client.request server.getProcessDiagnostics: server 2.6 ms, network 522 ms, render 871 ms. 18910b78-1451: server 0.4 ms, network 726 ms. 18910b78-1461 previewAutomation.focusHost: server 2.0 ms, network 898 ms.
  • 18910b78-1260 to -1302 and the 16:13Z to 16:26Z process.sample records: the desktop host (which runs the server in-process) used 0.1 to 0.5 % CPU, with private memory flat at 86 to 92 MB. The WebContent process is not sampled, and jsHeapBytes is null on WebKit.
  • No other RPCs were issued during the scroll, consistent with the whole thread already being on the client.

Suspected source

  • apps/web/src/components/chat/MessagesTimeline.tsx:552-583: LegendList with recycleItems={false}, estimatedItemSize={90} and maintainVisibleContentPosition={{ data: true, size: false }}, plus [overflow-anchor:none]. Rows that leave the render range unmount and later remount from scratch. Underestimated heights with no size anchoring make content jump as rows above the viewport are measured, which pulls in more rows to mount.
  • apps/web/src/components/ChatMarkdown.tsx:1566: every mount runs the full react-markdown pipeline (remark-gfm, then rehype-raw, which re-parses through parse5, then rehype-sanitize), with no cache of parsed output per message.
  • apps/web/src/components/ChatMarkdown.tsx:696: UncachedShikiCodeBlock calls highlighter.codeToHtml synchronously inside useMemo during render. The cache at :148 only helps blocks already seen, and scrolling into old turns mostly reaches blocks not seen before.
  • apps/web/src/components/chat/MessagesTimeline.tsx:1593: collapsed user messages still render their full markdown and only clip it with max-h-44.

Ruled out by code reading: older-history pagination (there is none), the 2 s status-bar server.getProcessDiagnostics poll (AppStatusBar is a sibling of the timeline and does not re-render it), and live-follow fighting the user (it is cancelled on the first wheel event).

Reproduction

  1. In the desktop app, open a long conversation with many assistant turns, code blocks and long pasted prompts.
  2. With the Profiler on, scroll from the bottom to the top with a trackpad.
  3. Run profiler events --kind client.batch --since 10m --json. Expect batches with 1 s or more of total stalls, interactions of 500 ms or more, and windowMs about 10 s.
  4. To confirm the cause, run vp run dev, open the same thread in Chrome and record a Performance trace while scrolling up. Expect long tasks dominated by row mounts, with unified, hast and parse5 work (rehype-raw) and codeToHtml under ChatMarkdown, followed by layout.

Root-cause hypothesis

Scrolling up mounts many timeline rows from scratch, and each mount does heavy synchronous work on the main thread. That work is a full markdown parse including rehype-raw, plus Shiki highlighting of code blocks seen for the first time. Missing size anchoring adds jumps and extra mounts. Confidence: medium. The profiler shows the main thread saturated during scrolling with an idle server and no RPC work, and the code path fits, but there is no trace attributing the time yet.

Suggested fix direction

  • Cache parsed markdown output per message, keyed by message id and text, in a bounded LRU, so remounts are cheap. Apply rehype-raw only when the text contains raw HTML.
  • Move Shiki highlighting off the main thread (the existing DiffWorkerPoolProvider worker pattern), or render a plain <pre> first and highlight in a transition or idle callback.
  • Render only a truncated slice of collapsed user messages.
  • Enable size anchoring (maintainVisibleContentPosition.size: true) and give realistic per-type estimates through getEstimatedItemSize.
  • Optionally, mount expensive row content in startTransition so scroll input stays responsive.

Activity

  1. added
    bugSomething is broken or behaving incorrectly.
    on Oct 14, 2026
  2. mubeda commented on Oct 14, 2026

    @mubeda
    OwnerAuthor

    New occurrence of client.interaction-1bb57fd93dcf (profiler fingerprint 1bb57fd93dcf):

    Field Value
    BiBCode 0.9.2, desktop (DMG) (the issue reported 0.9.2)
    OS macOS 27.0.1, arm64
    Count 8 (4 at the last report), last seen 2026-10-14T19:12:15Z

    What is new: the slowest interaction rose from 736 ms to 26,464 ms (97f7ca61-5132: 4 slow interactions in 10 s), and a main-thread sample shows a second cause besides timeline row mounts.

    • A 5 s sample of the WebContent process at 19:14Z: about 73 % of main-thread time in WebCore::LabelsNodeList traversal, called from JavaScript labels[0]. Base UI's useAriaLabelledBy does this after every commit of every Switch, Checkbox and Radio that has no explicit label id, and each read walks the whole document. A long conversation enlarges that document, so this cost grows with the same thread size this issue describes.
    • Details and the fix direction: rpc.slow: Base UI label lookup on every render saturates the macOS webview #106. Re-measure this issue's scroll scenario after that fix, to separate the two causes.
  3. mubeda commented on Oct 15, 2026

    @mubeda
    OwnerAuthor

    Shipped in v0.9.3 (7a0c3b1, 05f2a2b).

    • Chat timeline: a finished message's code blocks render at once and are colored right after, one at a time, instead of all being highlighted while their rows appear. In a 100-turn test thread, the main-thread stall while scrolling to the top fell from about 2 s to under 0.7 s.
    • AI terminal: a program printing many short lines quickly no longer loses output. The server coalesces pending output, batches attach frames, and sends a full-state resync to a client that falls behind (2,097 gaps before, 0 after in the same run).

    Terminal scrolling itself showed no stalls in Chromium; on macOS it was most likely the #106 freeze, also fixed in this release. If scrolling still stalls on v0.9.3, please open a new issue against v0.9.3.

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

    bugSomething is broken or behaving incorrectly.profiler:1bb57fd93dcfclient.interaction profiler finding

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions