perf: lazy-load highlight.js in ToolCall for better code-splitting

ToolCall.tsx previously imported highlight.js statically, pulling the
entire hljs bundle into the main Chat chunk and preventing Vite from
splitting it into the lazy markdown chunk.

Now hljs is dynamically imported and loaded on first use. This:
- Removes the Vite build warning about mixed static/dynamic imports
- Reduces the Chat chunk by ~1.6 KB
- Consolidates hljs into the lazy markdown chunk
- Renders unhighlighted text instantly, then re-renders with syntax
  highlighting once hljs loads (typically < 100ms)
This commit is contained in:
Nicolas Varrot
2026-02-27 21:02:13 +00:00
parent e77a1c8dcb
commit 9ef1c8be0d

View File

@@ -1,6 +1,5 @@
import { useState, useCallback, useMemo, useEffect, useRef, memo } from 'react';
import { ChevronRight, ChevronDown, Check, Copy, WrapText, AlignLeft } from 'lucide-react';
import hljs from '../lib/highlight';
import { copyToClipboard } from '../lib/clipboard';
import { useT } from '../hooks/useLocale';
import { useTheme } from '../hooks/useTheme';
@@ -124,11 +123,16 @@ function isStructured(text: string): boolean {
return termHits / lines.length > 0.3;
}
/** Highlight code using highlight.js, returns HTML string or null */
// Lazy-loaded highlight.js instance (keeps hljs out of the main bundle)
let _hljs: typeof import('highlight.js/lib/core').default | null = null;
const _hljsPromise = import('../lib/highlight').then(m => { _hljs = m.default; return m.default; });
/** Highlight code using highlight.js, returns HTML string or null.
* Returns null until hljs is loaded (callers should re-render after load). */
function highlightCode(text: string): string | null {
if (!text || !isStructured(text)) return null;
if (!text || !isStructured(text) || !_hljs) return null;
try {
const result = hljs.highlightAuto(text);
const result = _hljs.highlightAuto(text);
return result.value;
} catch {
return null;
@@ -178,7 +182,11 @@ function CopyButton({ text }: { text: string }) {
}
export function HighlightedPre({ text, className, wrap }: { text: string; className: string; wrap?: boolean }) {
const highlighted = useMemo(() => highlightCode(text), [text]);
const [hljsReady, setHljsReady] = useState(!!_hljs);
useEffect(() => {
if (!_hljs) { _hljsPromise.then(() => setHljsReady(true)); }
}, []);
const highlighted = useMemo(() => highlightCode(text), [text, hljsReady]); // eslint-disable-line react-hooks/exhaustive-deps
const wrapClass = wrap ? 'whitespace-pre-wrap break-words overflow-x-hidden' : '';
if (highlighted) {