Skip to content

perf(desktop): cache parsed markdown across channel-switch remounts#1635

Merged
wesbillman merged 2 commits into
block:mainfrom
noahjalex:perf/channel-switch-markdown-cache
Jul 8, 2026
Merged

perf(desktop): cache parsed markdown across channel-switch remounts#1635
wesbillman merged 2 commits into
block:mainfrom
noahjalex:perf/channel-switch-markdown-cache

Conversation

@noahjalex

@noahjalex noahjalex commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Why

The timeline is keyed by channel id (scroll-restoration requirement), so every channel switch remounts all rows and re-runs each row's react-markdown parse. Markdown-heavy channels pay ~2x the switch cost of plain text, all in parse work.

What

  • Cache parsed markdown element trees in a bounded module LRU (markdown/nodeCache.ts), keyed by every parse input. Cleared on workspace switch.
  • Per-mount runtime (channels, imeta, callbacks) moves to MarkdownRuntimeContext, so cached trees never capture per-mount state.
  • Bypasses: content >32KB, active search, nudge-suppressed prose. Key segments are length-prefixed so crafted names/URLs can't collide.
  • CI gate: warm switches must parse zero markdown (markdown-parse-cache.spec.ts, smoke suite).

Verify

cd desktop && pnpm install && pnpm build && npx playwright install chromium
npx playwright test markdown-parse-cache.spec.ts    # gate: 0 fresh parses per warm switch
npx playwright test --config=playwright.perf.config.ts warm-switch-markdown.perf.ts  # timings
pnpm test                                           # cache-key unit tests

Results

Warm-switch medians, 8 switches, 4x CPU throttle, two before/after runs:

Scenario Before After
60 markdown-heavy rows 924 / 1045 ms 699 / 809 ms (−23%)
~50 plain-text rows 419 / 483 ms 403 / 402 ms

Parse counter: cold visit parses 59 rows; every warm switch after that parses 0.

Risk Assessment

Hot render path (every message row). Covered by the zero-parse CI gate, 8 cache-key unit tests (incl. collision cases), and 117 passing markdown e2e specs.

Known: custom-emoji.spec.ts "Bug 2" fails identically on unmodified main — pre-existing.

🤖 Generated with AI

noahjalex and others added 2 commits July 8, 2026 15:48
The timeline subtree is keyed by channel id (scroll-restoration
requirement), so every channel switch remounts all rows and re-runs
each row's synchronous react-markdown parse. Route the per-mount
runtime through context so the component map is module-stable, then
cache parsed element trees in a module-level LRU keyed by parse
inputs. Warm markdown-heavy switches drop ~23 percent wall time
(A/B: 1045ms -> 809ms and 924ms -> 699ms median at 4x CPU throttle,
warm-switch-markdown.perf.ts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Address the adversarial-review findings on the parse cache: skip the
parse entirely when a config-nudge suppresses the prose (it previously
parsed content that was never rendered), bypass the cache for oversized
content so large pastes cannot pin memory, length-prefix cache-key
segments so relay-controlled names/URLs cannot forge segment
boundaries, and single-source the components-variant token so the map
partitioning and key partitioning cannot drift. Adds a deterministic
CI gate (markdown-parse-cache.spec.ts) asserting warm channel switches
perform zero fresh parses, via a parse counter exposed through the e2e
bridge. Extracts MarkdownVideoPlayer to keep markdown.tsx under the
file-size limit, and corrects the benchmark's row-count label.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@noahjalex noahjalex force-pushed the perf/channel-switch-markdown-cache branch from ffda2b7 to 7ae7f65 Compare July 8, 2026 19:50
@noahjalex noahjalex marked this pull request as ready for review July 8, 2026 19:51
@wesbillman wesbillman merged commit 2b32469 into block:main Jul 8, 2026
25 checks passed
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