4.7 KiB
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/streamuses 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:
// 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:
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:
- Add cache lookup before calling the AI service.
- Store the response on cache miss.
- 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:
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
- Read ARCHITECTURE.md to understand the module you're optimizing.
- Pick one item from section 2.
- Add a Vitest benchmark (
performance.now()before/after) alongside your change. - 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.