Repository navigation
Description
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-1326to18910b78-1465client.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 example18910b78-1384: 12 stalls, 2,258 ms total, max 930 ms;-1459: 2,675 ms total).18910b78-1326,-1425,-1527client.batch: Event Timing interactions of 672, 528 and 736 ms.18910b78-1423client.requestserver.getProcessDiagnostics: server 2.6 ms, network 522 ms, render 871 ms.18910b78-1451: server 0.4 ms, network 726 ms.18910b78-1461previewAutomation.focusHost: server 2.0 ms, network 898 ms.18910b78-1260to-1302and the 16:13Z to 16:26Zprocess.samplerecords: 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, andjsHeapBytesis 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:LegendListwithrecycleItems={false},estimatedItemSize={90}andmaintainVisibleContentPosition={{ 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 fullreact-markdownpipeline (remark-gfm, thenrehype-raw, which re-parses through parse5, thenrehype-sanitize), with no cache of parsed output per message.apps/web/src/components/ChatMarkdown.tsx:696:UncachedShikiCodeBlockcallshighlighter.codeToHtmlsynchronously insideuseMemoduring render. The cache at:148only 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 withmax-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
- In the desktop app, open a long conversation with many assistant turns, code blocks and long pasted prompts.
- With the Profiler on, scroll from the bottom to the top with a trackpad.
- 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, andwindowMsabout 10 s. - 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) andcodeToHtmlunderChatMarkdown, 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-rawonly when the text contains raw HTML. - Move Shiki highlighting off the main thread (the existing
DiffWorkerPoolProviderworker 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 throughgetEstimatedItemSize. - Optionally, mount expensive row content in
startTransitionso scroll input stays responsive.
Activity
mubeda commentedon Oct 14, 2026
New occurrence of
client.interaction-1bb57fd93dcf(profiler fingerprint1bb57fd93dcf):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.sampleof the WebContent process at 19:14Z: about 73 % of main-thread time inWebCore::LabelsNodeListtraversal, called from JavaScriptlabels[0]. Base UI'suseAriaLabelledBydoes 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.fix(web): highlight finished code blocks between frames, not during r…
fix(terminal): stop dropping output when a program prints faster than…
merge: release v0.9.3 (PR #108)
mubeda commentedon Oct 15, 2026
Shipped in v0.9.3 (7a0c3b1, 05f2a2b).
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.