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[] => {
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<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 result = await copyTextToClipboard(code);
@@ -709,15 +743,21 @@ const MarkdownCodeBlock: React.FC<{
</div>
</div>
<div className="px-3 py-2.5">
<SyntaxHighlighter
language={language}
style={syntaxTheme}
customStyle={{ margin: 0, background: 'transparent', padding: 0, fontSize: 'var(--text-code)', lineHeight: 'var(--markdown-code-block-line-height)' }}
codeTagProps={{ style: { fontSize: 'var(--text-code)', lineHeight: 'var(--markdown-code-block-line-height)' } }}
PreTag="pre"
>
{code}
</SyntaxHighlighter>
{highlight ? (
<SyntaxHighlighter
language={language}
style={syntaxTheme}
customStyle={CODE_SHARED_STYLE}
codeTagProps={{ style: CODE_SHARED_STYLE }}
PreTag="pre"
>
{code}
</SyntaxHighlighter>
) : (
<pre style={CODE_SHARED_STYLE}>
<code style={CODE_SHARED_STYLE}>{code}</code>
</pre>
)}
</div>
</div>
);
@@ -1256,7 +1296,6 @@ const useFileReferenceInteractions = ({
observer.observe(container, {
childList: true,
subtree: true,
characterData: true,
});
container.addEventListener('click', handleClick);
+1 -1
View File
@@ -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]);