fix(ui): CodeBlock 添加防抖优化,减少流式输出时的高亮闪烁

- 使用 ref 跟踪上一次高亮的代码,避免重复高亮
- 添加 150ms 防抖延迟,减少流式输出时的高亮次数
- 优化渲染逻辑,未高亮时显示纯文本
This commit is contained in:
2025-12-15 23:18:07 +08:00
parent 92619df026
commit 76b1ae1573
+30 -25
View File
@@ -4,7 +4,7 @@
* 代码块组件,支持语法高亮和一键复制 * 代码块组件,支持语法高亮和一键复制
*/ */
import { useState, useEffect } from 'react'; import { useState, useEffect, useRef } from 'react';
import { Copy, Check } from 'lucide-react'; import { Copy, Check } from 'lucide-react';
import { codeToHtml } from 'shiki'; import { codeToHtml } from 'shiki';
import { cn } from '../utils/cn'; import { cn } from '../utils/cn';
@@ -18,37 +18,42 @@ interface CodeBlockProps {
export function CodeBlock({ code, language = 'text', className }: CodeBlockProps) { export function CodeBlock({ code, language = 'text', className }: CodeBlockProps) {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [highlightedHtml, setHighlightedHtml] = useState<string>(''); const [highlightedHtml, setHighlightedHtml] = useState<string>('');
const [isLoading, setIsLoading] = useState(true); // 使用 ref 跟踪上一次高亮的代码,避免不必要的重新高亮
const lastHighlightedCode = useRef<string>('');
const highlightTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
// 语法高亮 // 语法高亮 - 使用防抖避免频繁高亮导致闪烁
useEffect(() => { useEffect(() => {
let cancelled = false; // 清除之前的定时器
if (highlightTimer.current) {
clearTimeout(highlightTimer.current);
}
async function highlight() { // 如果代码没变,不需要重新高亮
if (code === lastHighlightedCode.current && highlightedHtml) {
return;
}
// 使用防抖:等待 150ms 后再进行高亮(流式输出时减少高亮次数)
highlightTimer.current = setTimeout(async () => {
try { try {
const html = await codeToHtml(code, { const html = await codeToHtml(code, {
lang: language, lang: language,
theme: 'github-dark', theme: 'github-dark',
}); });
if (!cancelled) { lastHighlightedCode.current = code;
setHighlightedHtml(html); setHighlightedHtml(html);
}
} catch { } catch {
// 如果语言不支持,回退到纯文本 // 如果语言不支持,回退到纯文本
if (!cancelled) { lastHighlightedCode.current = code;
setHighlightedHtml(`<pre><code>${escapeHtml(code)}</code></pre>`); setHighlightedHtml(`<pre><code>${escapeHtml(code)}</code></pre>`);
}
} finally {
if (!cancelled) {
setIsLoading(false);
}
} }
} }, 150);
highlight();
return () => { return () => {
cancelled = true; if (highlightTimer.current) {
clearTimeout(highlightTimer.current);
}
}; };
}, [code, language]); }, [code, language]);
@@ -82,17 +87,17 @@ export function CodeBlock({ code, language = 'text', className }: CodeBlockProps
</button> </button>
</div> </div>
{/* 代码内容 */} {/* 代码内容 - 始终显示高亮后的内容,未高亮时显示纯文本 */}
<div className="overflow-x-auto"> <div className="overflow-x-auto">
{isLoading ? ( {highlightedHtml ? (
<pre className="p-4 bg-code text-fg-secondary text-sm font-mono">
<code>{code}</code>
</pre>
) : (
<div <div
className="shiki-wrapper text-sm [&>pre]:p-4 [&>pre]:m-0 [&>pre]:bg-code" className="shiki-wrapper text-sm [&>pre]:p-4 [&>pre]:m-0 [&>pre]:bg-code"
dangerouslySetInnerHTML={{ __html: highlightedHtml }} dangerouslySetInnerHTML={{ __html: highlightedHtml }}
/> />
) : (
<pre className="p-4 bg-code text-fg-secondary text-sm font-mono">
<code>{code}</code>
</pre>
)} )}
</div> </div>
</div> </div>