mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-07 08:26:00 +00:00
perf: optimize file tree and preview rendering performance
- Add useMemo to cache tree structure and syntax highlighting results - Add React.memo to FileTreeNodeComponent to prevent unnecessary re-renders - Add useCallback for stable callback references - Simplify CSS styles (remove gradients, blur, shadows) to reduce GPU load Fixes performance issues with file preview lag reported by users.
This commit is contained in:
parent
1f5b1f76fc
commit
2e8782b6e1
4 changed files with 56 additions and 34 deletions
|
|
@ -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 (
|
||||
<div className={className}>
|
||||
{/* Reuse the same wrapper styling as Markdown code blocks */}
|
||||
<div className="my-6 rounded-2xl border border-border/60 bg-gradient-to-br from-secondary/45 via-background to-secondary/20 p-1 shadow-sm">
|
||||
<div className="max-w-full overflow-x-auto rounded-xl bg-background/80 px-4 py-4 backdrop-blur-sm">
|
||||
{/* Simplified styling - removed gradient, blur, and shadow for better performance */}
|
||||
<div className="my-4 rounded-lg border border-border/60 bg-secondary/30">
|
||||
<div className="max-w-full overflow-x-auto rounded-lg bg-background px-4 py-3">
|
||||
<pre className="m-0 min-w-max bg-transparent p-0 text-[13px] leading-6">
|
||||
<code
|
||||
className="hljs"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from 'react'
|
||||
import { useState, memo, useMemo } from 'react'
|
||||
import { ChevronRight, ChevronDown, Folder, FolderOpen, FileText, FileCode, File } from 'lucide-react'
|
||||
import type { FileTreeNode } from './file-tree-builder'
|
||||
import { getFileIcon } from './file-type-utils'
|
||||
|
|
@ -33,14 +33,23 @@ function formatFileSize(bytes: number): string {
|
|||
/**
|
||||
* Recursive file tree node component.
|
||||
* Renders either a file or directory node with expand/collapse functionality.
|
||||
* Memoized to prevent unnecessary re-renders when parent updates.
|
||||
*/
|
||||
export function FileTreeNodeComponent({ node, onFileClick, defaultExpanded = false }: FileTreeNodeProps) {
|
||||
export const FileTreeNodeComponent = memo(function FileTreeNodeComponent({
|
||||
node,
|
||||
onFileClick,
|
||||
defaultExpanded = false,
|
||||
}: FileTreeNodeProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(defaultExpanded)
|
||||
|
||||
// Cache icon computation
|
||||
const IconComponent = useMemo(
|
||||
() => getIconComponent(getFileIcon(node.name)),
|
||||
[node.name]
|
||||
)
|
||||
|
||||
// Render file node
|
||||
if (node.type === 'file') {
|
||||
const IconComponent = getIconComponent(getFileIcon(node.name))
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between px-3 py-2 hover:bg-accent/10 cursor-pointer transition-colors group"
|
||||
|
|
@ -94,4 +103,4 @@ export function FileTreeNodeComponent({ node, onFileClick, defaultExpanded = fal
|
|||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={containerClassName}>
|
||||
|
|
@ -103,8 +110,8 @@ export function MarkdownRenderer({ content, className }: MarkdownRendererProps)
|
|||
</li>
|
||||
),
|
||||
pre: ({ children }) => (
|
||||
<div className="my-6 rounded-2xl border border-border/60 bg-gradient-to-br from-secondary/45 via-background to-secondary/20 p-1 shadow-sm">
|
||||
<div className="max-w-full overflow-x-auto rounded-xl bg-background/80 px-4 py-4 backdrop-blur-sm">
|
||||
<div className="my-4 rounded-lg border border-border/60 bg-secondary/30">
|
||||
<div className="max-w-full overflow-x-auto rounded-lg bg-background px-4 py-3">
|
||||
<pre className="m-0 min-w-max bg-transparent p-0 text-[13px] leading-6">{children}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -132,7 +139,7 @@ export function MarkdownRenderer({ content, className }: MarkdownRendererProps)
|
|||
blockquote: ({ className: blockquoteClassName, children, ...props }) => (
|
||||
<blockquote
|
||||
className={cn(
|
||||
'relative my-6 overflow-hidden rounded-r-xl border-l-4 border-l-primary/35 bg-secondary/30 px-5 py-4 text-foreground/80 shadow-sm',
|
||||
'my-4 border-l-4 border-l-primary/40 bg-secondary/20 px-4 py-3 text-foreground/80',
|
||||
blockquoteClassName
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -144,7 +151,7 @@ export function MarkdownRenderer({ content, className }: MarkdownRendererProps)
|
|||
<hr className={cn('my-10 mx-auto w-full max-w-full border-border/50', hrClassName)} {...props} />
|
||||
),
|
||||
table: ({ children }) => (
|
||||
<div className="my-6 overflow-hidden rounded-2xl border border-border/80 bg-card/80 shadow-sm">
|
||||
<div className="my-4 overflow-hidden rounded-lg border border-border/60 bg-card/80">
|
||||
<div className="max-w-full overflow-x-auto">
|
||||
<table className="m-0 min-w-full border-separate border-spacing-0 text-sm">{children}</table>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue