Skip to content

Commit 6f65d0c

Browse files
authored
Merge pull request #4 from ZengLiangYi/codex/client-lazy-load-graph
perf(client): lazy load pages and trim graph bundle
2 parents 5ef9f22 + 83537c9 commit 6f65d0c

8 files changed

Lines changed: 143 additions & 256 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@
1919
- **Coverage for memory services and routes** — Added regression tests across schemas, project-key derivation, recall, writeback, backfill, origin creation, decision logic, and HTTP route behavior.
2020
- **Real HTTP + MCP smoke coverage** — Verified the full writeback/recall loop through a real Fastify process and MCP stdio server with a mock embedding backend.
2121

22+
### Client Performance
23+
24+
- **Route-level lazy loading** — Switched the main client routes to lazy-loaded page bundles with a shared suspense fallback so the initial app payload is smaller.
25+
- **Relation graph canvas split** — Moved the force-graph implementation into a lazily loaded `RelationGraphCanvas` component so the graph runtime is only loaded when the graph page is opened.
26+
- **Lighter markdown code blocks** — Replaced `react-syntax-highlighter` with native `<pre><code>` rendering and removed the related client dependencies from the bundle.
27+
2228
## [0.4.0] - 2026-04-10
2329

2430
### New Data Sources

client/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
"react-i18next": "^17.0.2",
2424
"react-markdown": "^10.1.0",
2525
"react-router-dom": "^7.13.2",
26-
"react-syntax-highlighter": "^16.1.1",
2726
"remark-gfm": "^4.0.1",
2827
"tailwind-merge": "^3.5.0",
2928
"tailwindcss": "^4.2.2"
@@ -33,7 +32,6 @@
3332
"@types/node": "^24.12.0",
3433
"@types/react": "^19.2.14",
3534
"@types/react-dom": "^19.2.3",
36-
"@types/react-syntax-highlighter": "^15.5.13",
3735
"@vitejs/plugin-react": "^6.0.1",
3836
"eslint": "^9.39.4",
3937
"eslint-plugin-react-hooks": "^7.0.1",

client/src/App.tsx

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1+
import { Suspense, lazy, type ReactNode } from 'react';
12
import { BrowserRouter, Routes, Route } from 'react-router-dom';
23
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4+
import { useTranslation } from 'react-i18next';
35
import { ThemeProvider } from '@/providers/ThemeProvider.tsx';
46
import '@/i18n';
57
import { Layout } from '@/components/Layout.tsx';
6-
import { Dashboard } from '@/pages/Dashboard.tsx';
7-
import { Conversations } from '@/pages/Conversations.tsx';
8-
import { ConversationDetail } from '@/pages/ConversationDetail.tsx';
9-
import { Notes } from '@/pages/Notes.tsx';
10-
import { NoteDetail } from '@/pages/NoteDetail.tsx';
11-
import { SearchPage } from '@/pages/SearchPage.tsx';
12-
import { SettingsPage } from '@/pages/SettingsPage.tsx';
13-
import { RelationGraph } from '@/pages/RelationGraph.tsx';
8+
9+
const DashboardPage = lazy(() => import('@/pages/Dashboard.tsx').then((module) => ({ default: module.Dashboard })));
10+
const ConversationsPage = lazy(() => import('@/pages/Conversations.tsx').then((module) => ({ default: module.Conversations })));
11+
const ConversationDetailPage = lazy(() => import('@/pages/ConversationDetail.tsx').then((module) => ({ default: module.ConversationDetail })));
12+
const NotesPage = lazy(() => import('@/pages/Notes.tsx').then((module) => ({ default: module.Notes })));
13+
const NoteDetailPage = lazy(() => import('@/pages/NoteDetail.tsx').then((module) => ({ default: module.NoteDetail })));
14+
const SearchPage = lazy(() => import('@/pages/SearchPage.tsx').then((module) => ({ default: module.SearchPage })));
15+
const RelationGraphPage = lazy(() => import('@/pages/RelationGraph.tsx').then((module) => ({ default: module.RelationGraph })));
16+
const SettingsPage = lazy(() => import('@/pages/SettingsPage.tsx').then((module) => ({ default: module.SettingsPage })));
1417

1518
const queryClient = new QueryClient({
1619
defaultOptions: {
@@ -21,21 +24,30 @@ const queryClient = new QueryClient({
2124
},
2225
});
2326

27+
function RouteSuspense({ children }: { children: ReactNode }) {
28+
const { t } = useTranslation();
29+
return (
30+
<Suspense fallback={<div className="p-6 text-muted">{t('status.loading')}</div>}>
31+
{children}
32+
</Suspense>
33+
);
34+
}
35+
2436
export default function App() {
2537
return (
2638
<QueryClientProvider client={queryClient}>
2739
<ThemeProvider>
2840
<BrowserRouter>
2941
<Routes>
3042
<Route element={<Layout />}>
31-
<Route path="/" element={<Dashboard />} />
32-
<Route path="/conversations" element={<Conversations />} />
33-
<Route path="/conversations/:id" element={<ConversationDetail />} />
34-
<Route path="/notes" element={<Notes />} />
35-
<Route path="/notes/:id" element={<NoteDetail />} />
36-
<Route path="/search" element={<SearchPage />} />
37-
<Route path="/graph" element={<RelationGraph />} />
38-
<Route path="/settings" element={<SettingsPage />} />
43+
<Route path="/" element={<RouteSuspense><DashboardPage /></RouteSuspense>} />
44+
<Route path="/conversations" element={<RouteSuspense><ConversationsPage /></RouteSuspense>} />
45+
<Route path="/conversations/:id" element={<RouteSuspense><ConversationDetailPage /></RouteSuspense>} />
46+
<Route path="/notes" element={<RouteSuspense><NotesPage /></RouteSuspense>} />
47+
<Route path="/notes/:id" element={<RouteSuspense><NoteDetailPage /></RouteSuspense>} />
48+
<Route path="/search" element={<RouteSuspense><SearchPage /></RouteSuspense>} />
49+
<Route path="/graph" element={<RouteSuspense><RelationGraphPage /></RouteSuspense>} />
50+
<Route path="/settings" element={<RouteSuspense><SettingsPage /></RouteSuspense>} />
3951
</Route>
4052
</Routes>
4153
</BrowserRouter>

client/src/components/MarkdownRenderer.tsx

Lines changed: 3 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,6 @@
11
import { memo } from 'react';
22
import Markdown from 'react-markdown';
33
import remarkGfm from 'remark-gfm';
4-
import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter';
5-
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
6-
7-
// Register only languages commonly seen in AI coding conversations
8-
import typescript from 'react-syntax-highlighter/dist/esm/languages/prism/typescript';
9-
import javascript from 'react-syntax-highlighter/dist/esm/languages/prism/javascript';
10-
import tsx from 'react-syntax-highlighter/dist/esm/languages/prism/tsx';
11-
import jsx from 'react-syntax-highlighter/dist/esm/languages/prism/jsx';
12-
import python from 'react-syntax-highlighter/dist/esm/languages/prism/python';
13-
import bash from 'react-syntax-highlighter/dist/esm/languages/prism/bash';
14-
import json from 'react-syntax-highlighter/dist/esm/languages/prism/json';
15-
import css from 'react-syntax-highlighter/dist/esm/languages/prism/css';
16-
import sql from 'react-syntax-highlighter/dist/esm/languages/prism/sql';
17-
import yaml from 'react-syntax-highlighter/dist/esm/languages/prism/yaml';
18-
import markdown from 'react-syntax-highlighter/dist/esm/languages/prism/markdown';
19-
import rust from 'react-syntax-highlighter/dist/esm/languages/prism/rust';
20-
import go from 'react-syntax-highlighter/dist/esm/languages/prism/go';
21-
import java from 'react-syntax-highlighter/dist/esm/languages/prism/java';
22-
import csharp from 'react-syntax-highlighter/dist/esm/languages/prism/csharp';
23-
24-
SyntaxHighlighter.registerLanguage('typescript', typescript);
25-
SyntaxHighlighter.registerLanguage('javascript', javascript);
26-
SyntaxHighlighter.registerLanguage('tsx', tsx);
27-
SyntaxHighlighter.registerLanguage('jsx', jsx);
28-
SyntaxHighlighter.registerLanguage('python', python);
29-
SyntaxHighlighter.registerLanguage('bash', bash);
30-
SyntaxHighlighter.registerLanguage('shell', bash);
31-
SyntaxHighlighter.registerLanguage('json', json);
32-
SyntaxHighlighter.registerLanguage('css', css);
33-
SyntaxHighlighter.registerLanguage('sql', sql);
34-
SyntaxHighlighter.registerLanguage('yaml', yaml);
35-
SyntaxHighlighter.registerLanguage('markdown', markdown);
36-
SyntaxHighlighter.registerLanguage('rust', rust);
37-
SyntaxHighlighter.registerLanguage('go', go);
38-
SyntaxHighlighter.registerLanguage('java', java);
39-
SyntaxHighlighter.registerLanguage('csharp', csharp);
40-
SyntaxHighlighter.registerLanguage('cs', csharp);
414

425
interface MarkdownRendererProps {
436
content: string;
@@ -67,19 +30,9 @@ export const MarkdownRenderer = memo(function MarkdownRenderer({
6730
<div className="cc-code-header">
6831
<span className="cc-code-lang">{match[1]}</span>
6932
</div>
70-
<SyntaxHighlighter
71-
style={vscDarkPlus}
72-
language={match[1]}
73-
PreTag="div"
74-
customStyle={{
75-
margin: 0,
76-
borderRadius: '0 0 6px 6px',
77-
background: 'var(--code-bg)',
78-
fontSize: '13px',
79-
}}
80-
>
81-
{String(children).replace(/\n$/, '')}
82-
</SyntaxHighlighter>
33+
<pre className="cc-code-body">
34+
<code {...props}>{String(children).replace(/\n$/, '')}</code>
35+
</pre>
8336
</div>
8437
);
8538
},
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { MutableRefObject } from 'react';
2+
import ForceGraph2D, { type ForceGraphMethods } from 'react-force-graph-2d';
3+
4+
export interface GraphNode {
5+
id: number;
6+
title: string;
7+
project_name: string;
8+
tags: string[];
9+
color: string;
10+
val: number;
11+
x?: number;
12+
y?: number;
13+
}
14+
15+
export interface GraphLink {
16+
source: number;
17+
target: number;
18+
type: string;
19+
confidence: number;
20+
color: string;
21+
}
22+
23+
interface RelationGraphCanvasProps {
24+
graphRef: MutableRefObject<ForceGraphMethods<any, any> | undefined>;
25+
graphData: {
26+
nodes: GraphNode[];
27+
links: GraphLink[];
28+
};
29+
paintNode: (node: GraphNode, ctx: CanvasRenderingContext2D, globalScale: number) => void;
30+
paintLink: (link: GraphLink, ctx: CanvasRenderingContext2D, globalScale: number) => void;
31+
onNodeHover: (node: GraphNode | null) => void;
32+
onNodeClick: (node: GraphNode) => void;
33+
}
34+
35+
export function RelationGraphCanvas({
36+
graphRef,
37+
graphData,
38+
paintNode,
39+
paintLink,
40+
onNodeHover,
41+
onNodeClick,
42+
}: RelationGraphCanvasProps) {
43+
return (
44+
<ForceGraph2D
45+
ref={graphRef}
46+
graphData={graphData}
47+
nodeCanvasObject={paintNode}
48+
linkCanvasObject={paintLink}
49+
nodePointerAreaPaint={(node, color, ctx) => {
50+
const radius = Math.sqrt(node.val) * 3 + 3;
51+
ctx.beginPath();
52+
ctx.arc(node.x!, node.y!, radius, 0, Math.PI * 2);
53+
ctx.fillStyle = color;
54+
ctx.fill();
55+
}}
56+
onNodeHover={(node) => onNodeHover(node as GraphNode | null)}
57+
onNodeClick={(node) => onNodeClick(node as GraphNode)}
58+
nodeLabel={() => ''}
59+
cooldownTicks={100}
60+
warmupTicks={50}
61+
d3AlphaDecay={0.02}
62+
d3VelocityDecay={0.3}
63+
backgroundColor="transparent"
64+
/>
65+
);
66+
}

client/src/index.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,21 @@
9494
padding: 4px 12px;
9595
}
9696

97+
.cc-code-body {
98+
margin: 0;
99+
padding: 0.85rem 1rem;
100+
background: var(--code-bg);
101+
color: var(--text-primary);
102+
font-size: 13px;
103+
line-height: 1.6;
104+
overflow-x: auto;
105+
}
106+
107+
.cc-code-body code {
108+
display: block;
109+
white-space: pre;
110+
}
111+
97112
.cc-code-lang {
98113
font-family: var(--font-mono);
99114
font-size: 11px;

client/src/pages/RelationGraph.tsx

Lines changed: 25 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { useCallback, useMemo, useRef, useState } from 'react';
1+
import { Suspense, lazy, useCallback, useMemo, useRef, useState } from 'react';
22
import { useNavigate } from 'react-router-dom';
33
import { Loader2, ZoomIn, ZoomOut, Maximize } from 'lucide-react';
44
import { useTranslation } from 'react-i18next';
55
import { useQuery } from '@tanstack/react-query';
6-
import ForceGraph2D, { type ForceGraphMethods } from 'react-force-graph-2d';
76
import { api } from '@/lib/api.ts';
7+
import type { GraphLink, GraphNode } from '@/components/RelationGraphCanvas.tsx';
8+
import type { ForceGraphMethods } from 'react-force-graph-2d';
89

910
// =============================================
1011
// Constants
@@ -37,28 +38,11 @@ const PROJECT_COLORS = [
3738
'#06b6d4', '#eab308', '#ec4899', '#14b8a6', '#8b5cf6',
3839
];
3940

40-
// =============================================
41-
// Types for ForceGraph
42-
// =============================================
43-
44-
interface GraphNode {
45-
id: number;
46-
title: string;
47-
project_name: string;
48-
tags: string[];
49-
color: string;
50-
val: number;
51-
x?: number;
52-
y?: number;
53-
}
54-
55-
interface GraphLink {
56-
source: number;
57-
target: number;
58-
type: string;
59-
confidence: number;
60-
color: string;
61-
}
41+
const RelationGraphCanvas = lazy(() =>
42+
import('@/components/RelationGraphCanvas.tsx').then((module) => ({
43+
default: module.RelationGraphCanvas,
44+
})),
45+
);
6246

6347
// =============================================
6448
// Component
@@ -259,27 +243,23 @@ export function RelationGraph() {
259243

260244
{/* Graph */}
261245
<div className="flex-1 relative overflow-hidden">
262-
<ForceGraph2D
263-
ref={graphRef}
264-
graphData={graphData}
265-
nodeCanvasObject={paintNode}
266-
linkCanvasObject={paintLink}
267-
nodePointerAreaPaint={(node, color, ctx) => {
268-
const radius = Math.sqrt(node.val) * 3 + 3;
269-
ctx.beginPath();
270-
ctx.arc(node.x!, node.y!, radius, 0, Math.PI * 2);
271-
ctx.fillStyle = color;
272-
ctx.fill();
273-
}}
274-
onNodeHover={(node) => setHoveredNode(node as GraphNode | null)}
275-
onNodeClick={(node) => navigate(`/notes/${node.id}`)}
276-
nodeLabel={() => ''} // We handle labels in paintNode
277-
cooldownTicks={100}
278-
warmupTicks={50}
279-
d3AlphaDecay={0.02}
280-
d3VelocityDecay={0.3}
281-
backgroundColor="transparent"
282-
/>
246+
<Suspense
247+
fallback={
248+
<div className="flex h-full items-center justify-center text-muted">
249+
<Loader2 size={18} className="animate-spin mr-2" />
250+
{isZh ? '加载图谱中...' : 'Loading graph...'}
251+
</div>
252+
}
253+
>
254+
<RelationGraphCanvas
255+
graphRef={graphRef as never}
256+
graphData={graphData}
257+
paintNode={paintNode}
258+
paintLink={paintLink}
259+
onNodeHover={setHoveredNode}
260+
onNodeClick={(node) => navigate(`/notes/${node.id}`)}
261+
/>
262+
</Suspense>
283263

284264
{/* Tooltip */}
285265
{hoveredNode && (

0 commit comments

Comments
 (0)