Repository navigation
perf(transcript): tail-first thread switch + parsed-markdown cache (#1475) - #1489
Conversation
Switching back to a big thread re-parsed every mounted answer: micromark, mdast and hast were ~33 ms of the ~75 ms switch frame on the real 3.3 MB thread. react-markdown's parse is a pure function of the text, so its element tree is now kept in an LRU bounded at 400k chars (a few tail windows) and mounted again on the next switch. The live tail of a streaming reply is not cached, so streaming can't evict settled parses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first render of a thread now mounts only the last 8k chars of message text, about a screen. On the real 3.3 MB thread the 120-entry tail window holds two 34-36 KB answers far above the fold, and parsing them was most of the first-open frame. After paint, the deferred answers are parsed into the markdown cache one per idle slot, then the rest of the window mounts from it in a transition. - A tail that doesn't fill the pane mounts everything before paint, so nothing visibly grows. - useStickToBottom pins before paint when entries mount above a stuck view (#607); plain appends still skip the layout read. - Show earlier and jump-to-message mount the full window at once (#564). - A tool run cut by the old start re-keys when its head mounts; it is now seeded as seen so it doesn't replay its entrance animation. The window-bounds SSR test now mounts, since SSR never runs the effect that fills the window in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tail-first layout effect had no deps, so on a streaming thread every thread:updated push (~5/s) re-ran it: a forced scrollHeight read per push, the layout cost #1482 removed, and a restarted idle chain that spent one slot per already-parsed answer and could starve until the stream ended. It now runs once per tail-only phase (keyed on the thread) and reads the deferred range through a ref. The idle chain skips cached answers without spending a slot. A test streams five pushes into the tail-only phase and asserts zero layout reads, one idle chain, then the full window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d94f718c01
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| onClick={() => { | ||
| fullWindowThread.current = threadId; | ||
| showEarlier(); | ||
| }} |
There was a problem hiding this comment.
Force a render when revealing the deferred window
When a thread has fewer than TRANSCRIPT_WINDOW entries but exceeds the first-paint character budget, windowStart is already 0 and only the derived start hides earlier entries. Clicking this button mutates the ref, but showEarlier() then applies extendWindowStart(0), so React bails out of the unchanged state update and the hidden entries remain absent until the idle preparse chain eventually completes. Trigger the full-window tick here (or otherwise update state) so “Show earlier” reveals the entries immediately, especially when a busy renderer delays several idle callbacks.
Useful? React with 👍 / 👎.
Part of #1475, round 3. In the end-to-end measurement (
docs/perf/2026-10-06-end-to-end.md), the big-thread switch was the one renderer item that barely moved (−8%; still one ~135 ms frame). The switch-frame breakdown is indocs/perf/2026-10-06-switch-frame.md.Frame breakdown before (headless Chromium, real 3.3 MB thread):
What changed
Markdown.tsx): an LRU of the parsed element tree, keyed by text, with a 400k-char budget. The still-streaming tail chunk is not cached. This is safe because react-markdown's defaultMarkdownis synchronous and hook-free, and the parse is a pure function of the text.ThreadView.tsx):startTransition.useStickToBottomre-pins when entries mount above a stuck view.Measured (worker, headless Chromium, median of 5; the ratios carry over to the app, the absolute times don't):
Verified independently
vite buildpasses.Known trade-off: for roughly the first 100 ms after a switch, answers above the first screen aren't mounted yet, so find-in-page or an immediate scroll-up sees less. Show earlier mounts everything.
🤖 Generated with Claude Code