From 9f81c8fa4653a41a39b71c6e24701ad57930fc6d Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Mon, 20 Apr 2026 20:52:57 +0300 Subject: [PATCH] perf: trim observer work and defer Prism during streaming - Drop characterData from scroll-follow and file-link MutationObservers; ResizeObserver already catches content growth. - extractMermaidBlocks: bail out when markdown has no 'mermaid' at all. - MarkdownCodeBlock: skip Prism highlighting while code is actively changing, render plain pre until settled for 300ms. --- .../src/components/chat/MarkdownRenderer.tsx | 59 +++++++++++++++---- packages/ui/src/hooks/useScrollEngine.ts | 2 +- 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/packages/ui/src/components/chat/MarkdownRenderer.tsx b/packages/ui/src/components/chat/MarkdownRenderer.tsx index 354d7238..b2457889 100644 --- a/packages/ui/src/components/chat/MarkdownRenderer.tsx +++ b/packages/ui/src/components/chat/MarkdownRenderer.tsx @@ -484,6 +484,7 @@ type MermaidControlOptions = { }; const extractMermaidBlocks = (markdown: string): string[] => { + if (!markdown.includes('mermaid')) return []; const blocks: string[] = []; const regex = /(?:^|\r?\n)(`{3,}|~{3,})mermaid[^\n\r]*\r?\n([\s\S]*?)\r?\n\1(?=\r?\n|$)/gi; let match: RegExpExecArray | null = regex.exec(markdown); @@ -679,12 +680,45 @@ const normalizeCodeBlockText = (code: string, language: string): string => { return decodeHtmlEntities(code); }; +const CODE_HIGHLIGHT_SETTLE_MS = 300; +const CODE_SHARED_STYLE: React.CSSProperties = { + margin: 0, + background: 'transparent', + padding: 0, + fontSize: 'var(--text-code)', + lineHeight: 'var(--markdown-code-block-line-height)', +}; + const MarkdownCodeBlock: React.FC<{ code: string; language: string; syntaxTheme: { [key: string]: React.CSSProperties }; }> = ({ code, language, syntaxTheme }) => { const [copied, setCopied] = React.useState(false); + const [highlight, setHighlight] = React.useState(true); + const prevCodeRef = React.useRef(code); + const timerRef = React.useRef | null>(null); + + // Defer Prism highlighting while code is actively streaming. + // Initial mount renders highlighted immediately (plays nice with finalized blocks). + React.useEffect(() => { + if (prevCodeRef.current === code) return; + prevCodeRef.current = code; + + if (timerRef.current) clearTimeout(timerRef.current); + setHighlight(false); + timerRef.current = setTimeout(() => { + setHighlight(true); + timerRef.current = null; + }, CODE_HIGHLIGHT_SETTLE_MS); + + return () => { + if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = null; + } + }; + }, [code]); const handleCopy = React.useCallback(async () => { const result = await copyTextToClipboard(code); @@ -709,15 +743,21 @@ const MarkdownCodeBlock: React.FC<{
- - {code} - + {highlight ? ( + + {code} + + ) : ( +
+            {code}
+          
+ )}
); @@ -1256,7 +1296,6 @@ const useFileReferenceInteractions = ({ observer.observe(container, { childList: true, subtree: true, - characterData: true, }); container.addEventListener('click', handleClick); diff --git a/packages/ui/src/hooks/useScrollEngine.ts b/packages/ui/src/hooks/useScrollEngine.ts index 8eeab49d..f381482b 100644 --- a/packages/ui/src/hooks/useScrollEngine.ts +++ b/packages/ui/src/hooks/useScrollEngine.ts @@ -159,7 +159,7 @@ export const useScrollEngine = ({ resize.observe(container); const mutation = new MutationObserver(onChange); - mutation.observe(container, { childList: true, subtree: true, characterData: true }); + mutation.observe(container, { childList: true, subtree: true }); followObserversRef.current = { resize, mutation }; }, [containerRef, runFollowBurst]);