Files
zui/PERFORMANCE_IMPROVEMENTS.md
2026-03-28 22:20:19 +01:00

4.7 KiB
Raw Permalink Blame History

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 responsesPOST /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.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:

  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:

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 6070% 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 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.