From ea510a7c02d9e26ad39b2c18c41bc51ce3b10803 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 21 May 2026 20:08:32 -0500 Subject: [PATCH] perf(desktop): memoize MarkdownText plugins to stop churning Streamdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inline `plugins={{ math: mathPlugin, ...(isStreaming ? {} : { code }) }}` on `` constructed a new object literal on every parent render. That broke ``'s outer memo and forced its internal `rehypePlugins` / `remarkPlugins` array useMemos to rebuild, which propagates a new identity into every `` and defeats Block's memoization for stable historical blocks. After memoizing on `[isStreaming]` (the only real dimension of variance), CPU profile during a 5 s synthetic stream on the 34 MB session shows `parser` self-time dropping out of the top 10, `compile` cut roughly in half, and `bn$1` / `m$1` (micromark internals) leaving the top entries. Doesn't move the visible longtask count on its own — Streamdown's per-Block parse cost still dominates whenever the last block's content changes — but it removes a class of unnecessary re-parses for historical blocks during streaming. See `scripts/profile-typing-lag.md` for the full investigation. --- .../src/components/assistant-ui/markdown-text.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/components/assistant-ui/markdown-text.tsx b/apps/desktop/src/components/assistant-ui/markdown-text.tsx index 00bbc1e866..80e598ba69 100644 --- a/apps/desktop/src/components/assistant-ui/markdown-text.tsx +++ b/apps/desktop/src/components/assistant-ui/markdown-text.tsx @@ -238,6 +238,16 @@ const HEADING_SIZES: Record<'h1' | 'h2' | 'h3' | 'h4', string> = { const MarkdownTextImpl = () => { const isStreaming = useAuiState(s => s.message.status?.type === 'running') + // Stable per-state plugin object. The previous inline `{ math: mathPlugin, + // ...(isStreaming ? {} : { code }) }` created a new object identity on every + // render, which churns Streamdown's outer memo + propagates new prop + // identities into every Block. The plugin set really only varies on + // `isStreaming`, so memoize on that. + const plugins = useMemo( + () => (isStreaming ? { math: mathPlugin } : { math: mathPlugin, code }), + [isStreaming] + ) + const components = useMemo( () => ({ @@ -331,7 +341,7 @@ const MarkdownTextImpl = () => { // on the SyntaxHighlighter component, so we don't pay code-block // tokenization on every token even with this set. parseIncompleteMarkdown - plugins={{ math: mathPlugin, ...(isStreaming ? {} : { code }) }} + plugins={plugins} preprocess={preprocessMarkdown} /> )