diff --git a/web/src/features/skill/code-renderer.tsx b/web/src/features/skill/code-renderer.tsx index 6784d607..ec4fca94 100644 --- a/web/src/features/skill/code-renderer.tsx +++ b/web/src/features/skill/code-renderer.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react' import { common, createLowlight } from 'lowlight' // Create lowlight instance with common languages @@ -14,30 +15,28 @@ interface CodeRendererProps { /** * Renders code with syntax highlighting using lowlight (highlight.js wrapper). * Reuses the same styling as Markdown code blocks for visual consistency. + * Memoized to prevent re-highlighting on every render. */ export function CodeRenderer({ code, language, className }: CodeRendererProps) { - let highlightedCode: string - - try { - if (language && lowlight.registered(language)) { - // Highlight with specified language - const tree = lowlight.highlight(language, code, { prefix: 'hljs-' }) - highlightedCode = treeToHtml(tree) - } else { - // Fallback to plain text (no highlighting) - highlightedCode = escapeHtml(code) + // Cache syntax highlighting result + const highlightedCode = useMemo(() => { + try { + if (language && lowlight.registered(language)) { + const tree = lowlight.highlight(language, code, { prefix: 'hljs-' }) + return treeToHtml(tree) + } + return escapeHtml(code) + } catch (error) { + console.error('Syntax highlighting failed:', error) + return escapeHtml(code) } - } catch (error) { - // If highlighting fails, escape HTML and display as plain text - console.error('Syntax highlighting failed:', error) - highlightedCode = escapeHtml(code) - } + }, [code, language]) return (
getIconComponent(getFileIcon(node.name)),
+ [node.name]
+ )
+
// Render file node
if (node.type === 'file') {
- const IconComponent = getIconComponent(getFileIcon(node.name))
-
return (
)
-}
+})
diff --git a/web/src/features/skill/file-tree.tsx b/web/src/features/skill/file-tree.tsx
index 96e1ec8b..9e9ef3f3 100644
--- a/web/src/features/skill/file-tree.tsx
+++ b/web/src/features/skill/file-tree.tsx
@@ -1,3 +1,4 @@
+import { useMemo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { Folder } from 'lucide-react'
import type { SkillFile } from '@/api/types'
@@ -18,13 +19,19 @@ interface FileTreeProps {
*/
export function FileTree({ files, onFileClick, bare }: FileTreeProps) {
const { t } = useTranslation()
- const tree = buildFileTree(files)
- const handleFileClick = (node: FileTreeNode) => {
- if (node.type === 'file' && onFileClick) {
- onFileClick(node)
- }
- }
+ // Cache tree structure to avoid rebuilding on every render
+ const tree = useMemo(() => buildFileTree(files), [files])
+
+ // Stable callback reference to prevent child re-renders
+ const handleFileClick = useCallback(
+ (node: FileTreeNode) => {
+ if (node.type === 'file' && onFileClick) {
+ onFileClick(node)
+ }
+ },
+ [onFileClick]
+ )
const treeContent = (
diff --git a/web/src/features/skill/markdown-renderer.tsx b/web/src/features/skill/markdown-renderer.tsx
index 31833567..1f787021 100644
--- a/web/src/features/skill/markdown-renderer.tsx
+++ b/web/src/features/skill/markdown-renderer.tsx
@@ -1,3 +1,4 @@
+import { useMemo } from 'react'
import ReactMarkdown from 'react-markdown'
import rehypeHighlight from 'rehype-highlight'
import rehypeSanitize from 'rehype-sanitize'
@@ -17,6 +18,7 @@ interface MarkdownRendererProps {
* Renders markdown from skill packages using a constrained plugin stack.
* Frontmatter is stripped before render because package metadata is surfaced in
* dedicated UI sections and should not appear twice in the document body.
+ * Memoized to prevent re-parsing on every render.
*/
export function MarkdownRenderer({ content, className }: MarkdownRendererProps) {
const containerClassName = [
@@ -25,7 +27,12 @@ export function MarkdownRenderer({ content, className }: MarkdownRendererProps)
]
.filter(Boolean)
.join(' ')
- const normalizedContent = stripMarkdownFrontmatter(content)
+
+ // Cache the normalized content to prevent re-parsing on every render
+ const normalizedContent = useMemo(
+ () => stripMarkdownFrontmatter(content),
+ [content]
+ )
return (
@@ -103,8 +110,8 @@ export function MarkdownRenderer({ content, className }: MarkdownRendererProps)
),
pre: ({ children }) => (
-
-
+
+
{children}
@@ -132,7 +139,7 @@ export function MarkdownRenderer({ content, className }: MarkdownRendererProps)
blockquote: ({ className: blockquoteClassName, children, ...props }) => (
),
table: ({ children }) => (
-
+
{children}