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:
@@ -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);
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
Reference in New Issue
Block a user