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.
This commit is contained in:
Bohdan Triapitsyn
2026-04-20 20:52:57 +03:00
parent ad7790b3ec
commit 9f81c8fa46
2 changed files with 50 additions and 11 deletions
@@ -484,6 +484,7 @@ type MermaidControlOptions = {
}; };
const extractMermaidBlocks = (markdown: string): string[] => { const extractMermaidBlocks = (markdown: string): string[] => {
if (!markdown.includes('mermaid')) return [];
const blocks: string[] = []; const blocks: string[] = [];
const regex = /(?:^|\r?\n)(`{3,}|~{3,})mermaid[^\n\r]*\r?\n([\s\S]*?)\r?\n\1(?=\r?\n|$)/gi; 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); let match: RegExpExecArray | null = regex.exec(markdown);
@@ -679,12 +680,45 @@ const normalizeCodeBlockText = (code: string, language: string): string => {
return decodeHtmlEntities(code); 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<{ const MarkdownCodeBlock: React.FC<{
code: string; code: string;
language: string; language: string;
syntaxTheme: { [key: string]: React.CSSProperties }; syntaxTheme: { [key: string]: React.CSSProperties };
}> = ({ code, language, syntaxTheme }) => { }> = ({ code, language, syntaxTheme }) => {
const [copied, setCopied] = React.useState(false); const [copied, setCopied] = React.useState(false);
const [highlight, setHighlight] = React.useState(true);
const prevCodeRef = React.useRef<string>(code);
const timerRef = React.useRef<ReturnType<typeof setTimeout> | 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 handleCopy = React.useCallback(async () => {
const result = await copyTextToClipboard(code); const result = await copyTextToClipboard(code);
@@ -709,15 +743,21 @@ const MarkdownCodeBlock: React.FC<{
</div> </div>
</div> </div>
<div className="px-3 py-2.5"> <div className="px-3 py-2.5">
<SyntaxHighlighter {highlight ? (
language={language} <SyntaxHighlighter
style={syntaxTheme} language={language}
customStyle={{ margin: 0, background: 'transparent', padding: 0, fontSize: 'var(--text-code)', lineHeight: 'var(--markdown-code-block-line-height)' }} style={syntaxTheme}
codeTagProps={{ style: { fontSize: 'var(--text-code)', lineHeight: 'var(--markdown-code-block-line-height)' } }} customStyle={CODE_SHARED_STYLE}
PreTag="pre" codeTagProps={{ style: CODE_SHARED_STYLE }}
> PreTag="pre"
{code} >
</SyntaxHighlighter> {code}
</SyntaxHighlighter>
) : (
<pre style={CODE_SHARED_STYLE}>
<code style={CODE_SHARED_STYLE}>{code}</code>
</pre>
)}
</div> </div>
</div> </div>
); );
@@ -1256,7 +1296,6 @@ const useFileReferenceInteractions = ({
observer.observe(container, { observer.observe(container, {
childList: true, childList: true,
subtree: true, subtree: true,
characterData: true,
}); });
container.addEventListener('click', handleClick); container.addEventListener('click', handleClick);
+1 -1
View File
@@ -159,7 +159,7 @@ export const useScrollEngine = ({
resize.observe(container); resize.observe(container);
const mutation = new MutationObserver(onChange); 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 }; followObserversRef.current = { resize, mutation };
}, [containerRef, runFollowBurst]); }, [containerRef, runFollowBurst]);