perf: reduce bundle size by using custom highlight.js subset

Replace highlight.js/lib/common (36 languages) with a curated subset
of 16 languages relevant for coding-assistant chat UIs. Both ToolCall
(direct hljs) and ChatMessage (rehype-highlight) now share the same
custom bundle from src/lib/highlight.ts.

Languages included: bash, css, diff, dockerfile, go, ini, javascript,
json, markdown, python, rust, shell, sql, typescript, xml, yaml.

Markdown chunk: 477KB → 336KB (-30%, -45KB gzipped)
This commit is contained in:
Nicolas Varrot
2026-02-13 06:11:46 +00:00
parent 8679fdc3a0
commit 6b0261f9c1
3 changed files with 84 additions and 3 deletions

View File

@@ -5,6 +5,7 @@ import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkBreaks from 'remark-breaks';
import rehypeHighlight from 'rehype-highlight';
import { rehypeHighlightOptions } from '../lib/highlight';
import type { ChatMessage as ChatMessageType, MessageBlock } from '../types';
import { ThinkingBlock } from './ThinkingBlock';
import { ThinkingIndicator } from './ThinkingIndicator';
@@ -169,7 +170,7 @@ const markdownComponents = { pre: CodeBlock, img: MarkdownImage, a: MarkdownLink
function renderTextBlocks(blocks: MessageBlock[]) {
return getTextBlocks(blocks).map((block, i) => (
<div key={`text-${i}`} className="markdown-body">
<ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]} rehypePlugins={[rehypeHighlight]} components={markdownComponents}>
<ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]} rehypePlugins={[[rehypeHighlight, rehypeHighlightOptions]]} components={markdownComponents}>
{autoFormatText((block as Extract<MessageBlock, { type: 'text' }>).text)}
</ReactMarkdown>
</div>
@@ -470,7 +471,7 @@ export function ChatMessageComponent({ message: rawMessage, onRetry, agentAvatar
{/* User-visible text */}
{message.blocks.length > 0 ? renderTextBlocks(message.blocks) : (
<div className="markdown-body">
<ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]} rehypePlugins={[rehypeHighlight]} components={markdownComponents}>
<ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]} rehypePlugins={[[rehypeHighlight, rehypeHighlightOptions]]} components={markdownComponents}>
{autoFormatText(message.content)}
</ReactMarkdown>
</div>

View File

@@ -1,6 +1,6 @@
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
import { ChevronRight, ChevronDown, Check, Copy, WrapText, AlignLeft } from 'lucide-react';
import hljs from 'highlight.js/lib/common';
import hljs from '../lib/highlight';
import { useT } from '../hooks/useLocale';
import { ImageBlock } from './ImageBlock';
import { useToolCollapse } from '../hooks/useToolCollapse';

80
src/lib/highlight.ts Normal file
View File

@@ -0,0 +1,80 @@
/**
* Custom highlight.js bundle with only the languages relevant for
* a coding-assistant chat UI. This replaces `highlight.js/lib/common`
* (36 languages) with a focused subset (~16), cutting bundle size significantly.
*/
import hljs from 'highlight.js/lib/core';
import type { LanguageFn } from 'highlight.js';
// Languages commonly seen in AI assistant conversations
import bash from 'highlight.js/lib/languages/bash';
import css from 'highlight.js/lib/languages/css';
import diff from 'highlight.js/lib/languages/diff';
import dockerfile from 'highlight.js/lib/languages/dockerfile';
import go from 'highlight.js/lib/languages/go';
import ini from 'highlight.js/lib/languages/ini';
import javascript from 'highlight.js/lib/languages/javascript';
import json from 'highlight.js/lib/languages/json';
import markdown from 'highlight.js/lib/languages/markdown';
import python from 'highlight.js/lib/languages/python';
import rust from 'highlight.js/lib/languages/rust';
import shell from 'highlight.js/lib/languages/shell';
import sql from 'highlight.js/lib/languages/sql';
import typescript from 'highlight.js/lib/languages/typescript';
import xml from 'highlight.js/lib/languages/xml';
import yaml from 'highlight.js/lib/languages/yaml';
hljs.registerLanguage('bash', bash);
hljs.registerLanguage('css', css);
hljs.registerLanguage('diff', diff);
hljs.registerLanguage('dockerfile', dockerfile);
hljs.registerLanguage('go', go);
hljs.registerLanguage('ini', ini);
hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('json', json);
hljs.registerLanguage('markdown', markdown);
hljs.registerLanguage('python', python);
hljs.registerLanguage('rust', rust);
hljs.registerLanguage('shell', shell);
hljs.registerLanguage('sql', sql);
hljs.registerLanguage('typescript', typescript);
hljs.registerLanguage('xml', xml);
hljs.registerLanguage('yaml', yaml);
// Aliases for hljs direct usage (ToolCall.tsx)
hljs.registerAliases(['sh', 'zsh'], { languageName: 'bash' });
hljs.registerAliases(['js', 'jsx'], { languageName: 'javascript' });
hljs.registerAliases(['ts', 'tsx'], { languageName: 'typescript' });
hljs.registerAliases(['py'], { languageName: 'python' });
hljs.registerAliases(['html'], { languageName: 'xml' });
hljs.registerAliases(['yml'], { languageName: 'yaml' });
hljs.registerAliases(['toml', 'cfg', 'conf'], { languageName: 'ini' });
hljs.registerAliases(['rs'], { languageName: 'rust' });
/**
* Language map for rehype-highlight (used in ChatMessage.tsx).
* rehype-highlight uses lowlight internally and accepts a `languages` record.
*/
export const rehypeHighlightLanguages: Record<string, LanguageFn> = {
bash, css, diff, dockerfile, go, ini, javascript, json,
markdown, python, rust, shell, sql, typescript, xml, yaml,
};
/**
* rehype-highlight options with our custom language subset.
*/
export const rehypeHighlightOptions = {
languages: rehypeHighlightLanguages,
aliases: {
bash: ['sh', 'zsh'],
javascript: ['js', 'jsx'],
typescript: ['ts', 'tsx'],
python: ['py'],
xml: ['html'],
yaml: ['yml'],
ini: ['toml', 'cfg', 'conf'],
rust: ['rs'],
},
} as const;
export default hljs;