perf: lazy-load ReactMarkdown and plugins for faster initial render
Move react-markdown, remark-gfm, remark-breaks, and rehype-highlight imports from eager to dynamic via a LazyMarkdown wrapper component. The ~336KB markdown bundle is now loaded on-demand after initial paint instead of blocking the critical rendering path. - Create LazyMarkdown component with Suspense fallback - Pre-load plugins in parallel on module init - Replace all ReactMarkdown usage in ChatMessage with LazyMarkdown
This commit is contained in:
@@ -1,11 +1,7 @@
|
||||
|
||||
import { useState, useCallback, useRef, useEffect, useMemo, memo } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
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 { LazyMarkdown } from './LazyMarkdown';
|
||||
import type { ChatMessage as ChatMessageType, MessageBlock } from '../types';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import { ThinkingBlock } from './ThinkingBlock';
|
||||
@@ -167,17 +163,13 @@ function MarkdownLink(props: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
|
||||
}
|
||||
|
||||
const markdownComponents = { pre: CodeBlock, img: MarkdownImage, a: MarkdownLink };
|
||||
// Hoisted to module scope to avoid creating new array references each render
|
||||
import type { PluggableList } from 'unified';
|
||||
const remarkPlugins: PluggableList = [remarkGfm, remarkBreaks];
|
||||
const rehypePlugins: PluggableList = [[rehypeHighlight, rehypeHighlightOptions]];
|
||||
|
||||
function renderTextBlocks(blocks: MessageBlock[]) {
|
||||
return getTextBlocks(blocks).map((block, i) => (
|
||||
<div key={`text-${i}`} className="markdown-body">
|
||||
<ReactMarkdown remarkPlugins={remarkPlugins} rehypePlugins={rehypePlugins} components={markdownComponents}>
|
||||
<LazyMarkdown components={markdownComponents}>
|
||||
{autoFormatText((block as Extract<MessageBlock, { type: 'text' }>).text)}
|
||||
</ReactMarkdown>
|
||||
</LazyMarkdown>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
@@ -480,9 +472,9 @@ export const ChatMessageComponent = memo(function ChatMessageComponent({ message
|
||||
{/* User-visible text */}
|
||||
{message.blocks.length > 0 ? renderTextBlocks(message.blocks) : (
|
||||
<div className="markdown-body">
|
||||
<ReactMarkdown remarkPlugins={remarkPlugins} rehypePlugins={rehypePlugins} components={markdownComponents}>
|
||||
<LazyMarkdown components={markdownComponents}>
|
||||
{autoFormatText(message.content)}
|
||||
</ReactMarkdown>
|
||||
</LazyMarkdown>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
50
src/components/LazyMarkdown.tsx
Normal file
50
src/components/LazyMarkdown.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { lazy, Suspense } from 'react';
|
||||
import type { Components } from 'react-markdown';
|
||||
import type { PluggableList } from 'unified';
|
||||
|
||||
const ReactMarkdown = lazy(() => import('react-markdown'));
|
||||
|
||||
// Pre-load plugins so they're ready when ReactMarkdown renders
|
||||
let _remarkPlugins: PluggableList | null = null;
|
||||
let _rehypePlugins: PluggableList | null = null;
|
||||
let _pluginsReady = false;
|
||||
|
||||
const pluginsPromise = Promise.all([
|
||||
import('remark-gfm').then(m => m.default),
|
||||
import('remark-breaks').then(m => m.default),
|
||||
import('rehype-highlight').then(m => m.default),
|
||||
import('../lib/highlight').then(m => m.rehypeHighlightOptions),
|
||||
]).then(([remarkGfm, remarkBreaks, rehypeHighlight, rehypeHighlightOptions]) => {
|
||||
_remarkPlugins = [remarkGfm, remarkBreaks];
|
||||
_rehypePlugins = [[rehypeHighlight, rehypeHighlightOptions]];
|
||||
_pluginsReady = true;
|
||||
});
|
||||
|
||||
// Trigger plugin loading immediately
|
||||
void pluginsPromise;
|
||||
|
||||
interface LazyMarkdownProps {
|
||||
children: string;
|
||||
remarkPlugins?: PluggableList;
|
||||
rehypePlugins?: PluggableList;
|
||||
components?: Components;
|
||||
}
|
||||
|
||||
function MarkdownInner({ children, remarkPlugins, rehypePlugins, components }: LazyMarkdownProps) {
|
||||
const remark = remarkPlugins ?? (_pluginsReady ? _remarkPlugins! : []);
|
||||
const rehype = rehypePlugins ?? (_pluginsReady ? _rehypePlugins! : []);
|
||||
|
||||
return (
|
||||
<ReactMarkdown remarkPlugins={remark} rehypePlugins={rehype} components={components}>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}
|
||||
|
||||
export function LazyMarkdown(props: LazyMarkdownProps) {
|
||||
return (
|
||||
<Suspense fallback={<span className="opacity-50">{props.children.slice(0, 200)}</span>}>
|
||||
<MarkdownInner {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user