# 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.*