109 lines
4.7 KiB
Markdown
109 lines
4.7 KiB
Markdown
# Performance Improvements Plan
|
||
|
||
## 1. Current State
|
||
|
||
The core rendering and state management architecture is already well-structured for performance:
|
||
|
||
- **Command/reducer store** — all mutations go through a pure reducer; individual selectors can prevent unnecessary re-renders.
|
||
- **Delta-based undo/redo** — only diffs are stored, not full graph snapshots (max 100 entries).
|
||
- **Plugin node registry** — node types are loaded once at app init, not dynamically on each render.
|
||
- **Streaming AI responses** — `POST /api/agent/stream` uses SSE so the UI updates incrementally.
|
||
|
||
The sections below identify remaining bottlenecks and concrete next steps.
|
||
|
||
---
|
||
|
||
## 2. Known Bottlenecks
|
||
|
||
| # | Issue | Location | Impact | Root Cause |
|
||
| --- | --- | --- | --- | --- |
|
||
| 1 | Unguarded re-renders on canvas | `CanvasPage.tsx` | Frame drops on large graphs | Components not subscribed to granular store slices |
|
||
| 2 | Nunjucks template resolution on every render | `rendering.ts` resolve step | Slow Config node updates | No memoization of template output keyed to input hash |
|
||
| 3 | Kroki SVG requests not deduplicated | `rendering.ts` render step | Redundant network calls | No in-flight request deduplication or client-side cache |
|
||
| 4 | Sidebar tree renders all items | `KosmosPage` recollection tree | Scrolling lag with many workspaces | No list virtualization |
|
||
| 5 | Backend cache not wired to agent routes | `agentRoutes.ts` | Repeated identical LLM calls | `InMemoryCache` and `rateLimiter` exist but are unused |
|
||
| 6 | Full graph serialized to localStorage on every change | `useGraphStateWithHistory` | Storage I/O on every keypress | No debounce on the persistence write |
|
||
| 7 | Initial bundle size | Vite build | Slow first load | Heavy deps (BlockNote, React Flow, Nunjucks) loaded eagerly |
|
||
|
||
---
|
||
|
||
## 3. Recommended Improvements
|
||
|
||
### 3.1 Granular Store Subscriptions
|
||
|
||
Zustand supports slice-level subscriptions. Node components should select only their own data slice:
|
||
|
||
```ts
|
||
// Instead of subscribing to the entire graph:
|
||
const node = useCanvasStore(s => s.graph.nodes.find(n => n.id === id))
|
||
```
|
||
|
||
This prevents all nodes from re-rendering when a single node changes.
|
||
|
||
### 3.2 Memoize Template Resolution
|
||
|
||
Cache the Nunjucks resolution output keyed to a hash of the template source plus variable inputs. Invalidate only when those inputs change:
|
||
|
||
```ts
|
||
const resolved = useMemo(
|
||
() => resolveTemplate(template, variables),
|
||
[templateHash, variableHash]
|
||
)
|
||
```
|
||
|
||
### 3.3 Deduplicate Kroki Requests
|
||
|
||
Add a simple in-flight map in the render step: if a request for the same PlantUML source is already pending, reuse its promise. Cache successful responses keyed to the source string with a short TTL (e.g. 5 minutes).
|
||
|
||
### 3.4 Wire Backend Cache and Rate Limiter
|
||
|
||
`InMemoryCache` and `rateLimiter` middleware are implemented in `backend/src/`. Connect them to `agentRoutes.ts`:
|
||
|
||
1. Add cache lookup before calling the AI service.
|
||
2. Store the response on cache miss.
|
||
3. Apply rate limiting per IP to prevent abuse.
|
||
|
||
### 3.5 Debounce localStorage Writes
|
||
|
||
Wrap the graph persistence call in a debounce (e.g., 300 ms) to avoid a write on every keystroke or node drag. The delta-based history already computes minimal diffs; the bottleneck is the frequency of writes.
|
||
|
||
### 3.6 Virtualize the Sidebar Tree
|
||
|
||
Integrate `react-arborist` (already installed) with virtualization enabled for the recollection sidebar when item count exceeds a threshold (~50).
|
||
|
||
### 3.7 Code Split Heavy Routes
|
||
|
||
Add lazy imports for the three heavy route components so the initial bundle only loads what the user navigates to:
|
||
|
||
```ts
|
||
const FluxRoute = lazy(() => import('./app/recollections/flux/FluxRoute'))
|
||
const LogosPage = lazy(() => import('./app/recollections/logos/LogosPage'))
|
||
const KatalogosPage = lazy(() => import('./app/recollections/katalogos/KatalogosPage'))
|
||
```
|
||
|
||
### 3.8 Enable Brotli Compression in Nginx
|
||
|
||
Add brotli/gzip compression to `frontend/nginx.conf` for JS, CSS, and SVG assets. This can cut transfer size by 60–70% for the JS bundle.
|
||
|
||
---
|
||
|
||
## 4. Success Metrics
|
||
|
||
| Metric | Current (estimated) | Target |
|
||
| --- | --- | --- |
|
||
| Frame time on 50-node canvas | ~16 ms | < 10 ms |
|
||
| Initial JS bundle (gzipped) | ~800 KB | < 600 KB |
|
||
| Repeated identical LLM calls | uncached | 0 network round-trips |
|
||
| localStorage write frequency | every change | debounced 300 ms |
|
||
|
||
---
|
||
|
||
## 5. Contribution Path
|
||
|
||
1. Read [ARCHITECTURE.md](ARCHITECTURE.md) to understand the module you're optimizing.
|
||
2. Pick one item from section 2.
|
||
3. Add a Vitest benchmark (`performance.now()` before/after) alongside your change.
|
||
4. Submit a PR with the benchmark results in the description and update this file's "Current" column.
|
||
|
||
*This plan is a living document; update the metrics table when improvements land.*
|