Skip to content

perf(messages): prepare a message once for its renderer and link previews - #3466

Merged
kalvinnchau merged 1 commit into
mainfrom
peon/perf-link-preview-prepare
Nov 13, 2026
Merged

kalvinnchau merged 1 commit into
mainfrom
peon/perf-link-preview-prepare

Conversation

@kalvinnchau

Copy link
Copy Markdown
Contributor

Problem

Link previews (#3219, #3225) find a message's preview URLs with previewLinks. For any body with an https:// link, that function called prepareMarkdown, then scanned the prepared content again with scanMarkdown, and kept no result. The renderer already prepares the same body through its own cache.

A link-bearing row therefore parsed its Markdown two or three more times than a row without links, on every mount. It did this again on each remount while scrolling or switching channels.

Change

  • markdown-preparation.ts:
    • Owns the cache of recent prepared bodies. recentResults and preparedBodies move here from MessageMarkdown.tsx, unchanged.
    • Exposes it as preparedMarkdown(content, scanned?).
    • prepareMarkdown takes an optional scanned callback that receives the scan it made. It stays pure and keeps no state.
  • MessageMarkdown.tsx reads through preparedMarkdown, with the same cache and budget as before.
  • preview-links.ts:
    • Uses preparedMarkdown. On a fresh preparation it takes the scan through the callback instead of scanning again.
    • Keeps recent preview link lists in a recentResults cache. The key is the content and exclusions; the weight counts both the key and the URLs. Cached lists are frozen.
  • preview-links.test.ts asserts that the renderer and previews together scan a body once. This fails on the previous code.

Why behaviour is unchanged

  • prepareMarkdown is pure, so a cached prepared body is identical to a fresh one.
  • The scan passed to the callback is the scan of the returned content, the same input previews scanned before.
  • The link list depends only on content and exclusions, which are both in the cache key.

Evidence

Interleaved A/B on 9 Linux and 9 macOS boxes:

  • 6 rounds × 2 samples per variant on each box, so n=108 per variant per OS.
  • 4× CPU throttle.
  • Values are the mean of per-box medians.

The baseline is main (7bb45482) with #3464 and #3465 applied. This change was measured on top of that. The measured revision handed the scan over through module state instead of a callback; the parse count is the same. A variant with preview scanning disabled entirely is the ceiling.

case metric Linux base Linux PR Δ macOS base macOS PR Δ ceiling Δ (Linux / macOS)
thread scroll open script ms 636.4 618.3 −2.8% 381.4 368.2 −3.5% −2.9% / −3.7%
task ms 997.3 976.1 −2.1% 600.6 588.1 −2.1% −1.4% / −2.0%
LoAF blocking ms 551.7 534.3 −3.1% 271.2 258.4 −4.7% −2.5% / −5.4%
first rows ms 937.1 914.1 −2.5% 561.5 546.2 −2.7% −1.8% / −2.7%
cold markdown channel open script ms 495.6 487.8 −1.6% 320.6 316.8 −1.2% −2.0% / −1.7%
fast scroll paging (markdown) LoAF blocking ms 1489.8 1444.2 −3.1% 83.7 83.5 −0.3% −2.3% / −5.3%
  • Thread scroll open improved on every box (9/9) for script and LoAF on both OSes.
  • The change recovers all the time that link-preview scanning costs in these scenarios.
  • An earlier A/B of desktop-v0.7.18 against the same baseline put thread scroll open at +2.6% script on both OSes, which this removes.

Checks

  • vitest run src/features/messages: 3119 pass.
  • link-previews.spec and markdown-documents.spec pass on chromium and webkit.
  • bin/pnpm check is clean.

🤖 Generated with Claude Code

…iews

finding a message's link previews prepared and scanned its markdown again
and kept no result, so every mount of a link-bearing row parsed its body
two or three more times than a row without links, and again on each
remount while scrolling. link previews now share the renderer's cache of
recent prepared bodies, take the scan a fresh preparation made instead of
scanning again, and keep recent preview link lists the same way.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Kalvin Chau <[email protected]>
@kalvinnchau
kalvinnchau marked this pull request as ready for review November 13, 2026 15:29
@kalvinnchau
kalvinnchau requested a review from a team as a code owner November 13, 2026 15:29

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 no blocking findings from me. the scan handed back from prepareMarkdown is the final scan of the normalized content, and a cache hit falls back to scanning the prepared content like before. MessageRow computes previewUrls in its own render before MessageMarkdown renders, so the preview-first order the new test covers is the one that happens in practice. the preview-link key includes the exclusions, and nothing downstream mutates the frozen lists.

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 approving, no blocking findings (details in the earlier review).

@kalvinnchau
kalvinnchau merged commit e487d37 into main Nov 13, 2026
66 checks passed
@kalvinnchau
kalvinnchau deleted the peon/perf-link-preview-prepare branch November 13, 2026 15:44
joshfriend added a commit that referenced this pull request Nov 13, 2026
* origin/main: (153 commits)
  feat(agents): preview where interrupted agents were working before resume (#3494)
  perf(messages): close the editor and delete dialog on submit (#3493)
  feat(messages): quote selected message text into the composer (#2392)
  perf(toast): scope docked clearance to the toast host (#3489)
  fix(unread): undo exact captured ordinary reads conditionally (#3473)
  fix(solo): keep navigation responsive during chat deletion (#3488)
  fix(channels): keep custom thread tab names until the tab closes (#3458)
  perf(relay): verify cached history in the native host (#3426)
  fix(settings): refine claude and typesafe key setup dialogs (#3391)
  Update bundled help for Bestie and agent settings (#3194)
  feat(profiles): share and show a local time zone (#3332)
  Revert "feat(link-previews): unfurl web links below messages by default" (#3480)
  Show disabled plugin chats in Solo (#3456)
  perf(messages): host one context menu per message list (#3465)
  test(claude): keep the quit-release test copy above the minimum version (#3477)
  fix(profiles): restore verified names from the device cache (#3382)
  fix(mentions): show addressed agents first in Solo audience picker (#2856)
  perf: count member access once and scope find highlights to painting (#3464)
  perf(messages): prepare a message once for its renderer and link previews (#3466)
  fix(composer): speed up long pastes and paste office text as text (#3441)
  ...

# Conflicts:
#	docs/plugin-authoring-guide.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants