โก Web Performance¶
๐ Overview¶
This page outlines the performance optimizations and architectural decisions made to achieve smooth 60 FPS rendering for large-scale visualli visualizations (5,000-10,000 nodes).
Repo: visualli.ai/backend/web/*
๐ฏ Core Web Vitals Targets¶
- LCP (Largest Contentful Paint): < 2.5s
- FID (First Input Delay): < 100ms
- CLS (Cumulative Layout Shift): < 0.1
- TBT (Total Blocking Time): < 400ms
- FPS (Frames Per Second): 60 FPS sustained, > 30 FPS minimum
๐๏ธ Architectural Decisions¶
1. ๐ท Web Worker for Data Processing¶
Decision: Offload JSONL parsing and layout calculations to a dedicated Web Worker.
Implementation:
- visualliParser.worker.ts handles all CPU-intensive operations off the main thread
- Parses .visualli JSONL format (meta + extensions + layers)
- Pre-calculates flat node layouts for ALL layers (70-400ms saved per navigation)
- Resolves node overlaps using iterative force-directed algorithm
- Returns serialized document with pre-calculated positions
Impact:
- LCP improved by 40-60%
- TBT reduced by eliminating main thread blocking during parsing
- Instant layer navigation (no recalculation needed)
2. ๐ฒ RBush Spatial Indexing¶
Decision: Use RBush R-tree for O(log n) viewport culling instead of O(n) filtering.
Implementation:
- RBushSpatialIndex class wraps RBush library
- Bulk-loads nodes for optimized tree construction (< 50ms for 10K nodes)
- Viewport queries complete in < 5ms
- Max entries per node: 16 (tuned from default 9)
Impact:
- Viewport queries: O(n) โ O(log n)
- Pan/zoom performance: 60 FPS sustained
- Memory overhead: ~15% vs linear search, but worth the query speed
3. โป๏ธ Konva Shape Pooling¶
Decision: Reuse Konva shape objects instead of creating/destroying on viewport changes.
Implementation:
- ShapePoolManager maintains pools of reusable node/edge shapes
- Shapes marked active/inactive instead of destroyed
- Automatic cleanup when inactive pool exceeds threshold
- Pool sizes: 50 initial, 1000 max, 150 cleanup threshold
Impact:
- Garbage collection pressure reduced by 70%
- Memory stable over 60-minute sessions (< 20% growth)
- Smoother panning/zooming (no GC pauses)
4. ๐ Lazy Loading & Code Splitting¶
Decision: Defer non-critical component loading to improve initial load metrics.
Implementation:
-
Sidebar Component:
- Lazy loaded using
React.lazy() - Rendered only after user interaction or idle callback
- Suspense fallback: Skeleton placeholder for smooth transition
- Lazy loaded using
-
Canvas Component:
- Deferred initialization via
requestIdleCallbackAPI - 500ms timeout fallback for browsers without idle callback support
- Prevents blocking main thread during initial paint
- Konva stage initialization delayed until DOM is interactive
- Deferred initialization via
-
ProcessingStatus Component:
- Lazy loaded with dedicated Suspense boundary
- Only loaded when file processing begins
- Isolated error boundary to prevent cascade failures
-
Route-Based Splitting:
React.lazy()+Suspensefor all feature routes- Each route bundle loaded on-demand (Settings, Projects, etc.)
- Shared components extracted to separate chunk via Vite's
manualChunks
Bundle Optimization:
- Initial bundle: 120 KB (gzipped) vs 200 KB before splitting
- Vendor chunk separation: React, Konva, and utilities in separate bundles
- Vite dynamic imports with prefetch hints for predicted user paths
Impact:
- Initial bundle size reduced by 40% (200 KB โ 120 KB gzipped)
- LCP improved by 35% by prioritizing critical UI shell
- Speed Index optimized (shell UI renders in < 1.2s)
- Time to Interactive (TTI) reduced by 500ms
- Subsequent navigation instant due to route preloading
5. ๐ Viewport Culling & Progressive Rendering¶
Decision: Only render nodes visible in viewport + 10% margin.
Implementation:
- Spatial index query with VIEWPORT_MARGIN = 0.1 padding
- Max 800 visible nodes enforced
- Progressive rendering in batches (75 nodes per batch) when > 30 nodes
- 5ms budget for culling queries
Impact:
- Rendering cost reduced by 90% for large graphs
- Stable 60 FPS even with 10K total nodes
- Batch rendering prevents UI freeze (< 18ms per frame)
6. ๐พ Memory Management¶
Decision: Active monitoring and cleanup to prevent memory leaks.
Implementation:
- memoryMonitor tracks heap usage and triggers warnings
- Shape pool cleanup at 150 inactive shapes
- Spatial index reset on layer navigation
- Target: 500MB, Warning: 600MB, Critical: 800MB
Impact:
- Memory growth < 20% over 60 minutes
- No memory leaks in production
- Automatic quality degradation if memory critical
๐ Performance Constants¶
โก FPS Targets¶
- Target FPS: 60
- Warning Threshold: 45 FPS
- Critical Threshold: 30 FPS (triggers quality downgrade)
โฑ๏ธ Timing Budgets (milliseconds)¶
- Viewport Query: < 5ms
- Spatial Index Build: < 50ms (10K nodes)
- Frame Time: < 16.67ms (p95 < 18ms)
- Drag Response: < 16ms
๐ง Memory Limits¶
- Target: 500 MB
- Warning: 600 MB
- Critical: 800 MB
๐ File Structure¶
frontend/web/src/
โโโ core/
โ โโโ performanceConstants.ts # Central performance config
โ โโโ renderConfig.ts # Quality levels & feature flags
โโโ core-lib/
โ โโโ workers/
โ โ โโโ visualliParser.worker.ts # Web Worker for parsing
โ โโโ utils/
โ โโโ spatialIndex.ts # RBush wrapper
โ โโโ shapePool.ts # Konva shape pooling
โ โโโ memoryMonitor.ts # Memory tracking
โโโ features/canvas/
โโโ hooks/
โ โโโ useViewportNodes.ts # Viewport culling hook
โโโ components/
โโโ VisualliMindMapCanvas.enhanced.tsx
Note: For complete repository structure, see FOLDER_STRUCTURE.md.
โจ Key Non-Negotiables For All Future Changes¶
- Web Workers are critical - Offload all heavy computation from main thread
- Spatial indexing is non-negotiable - RBush makes large graphs feasible
- Object pooling prevents GC pauses - Reuse shapes instead of recreating
- Lazy loading improves UX - Prioritize critical UI, defer the rest
- Viewport culling is mandatory - Never render what users can't see
๐ฎ Future Optimizations¶
๐ง Planned Improvements¶
- WebGL Renderer: For > 15K nodes (PixiJS integration)
- Virtual Scrolling: Infinite canvas with chunk loading
- Level-of-Detail (LOD): Simplified rendering at high zoom-out
- IndexedDB Caching: Persist parsed documents locally
๐ Performance Monitoring¶
- Continuous FPS tracking with warnings
- Memory usage alerts
- Automatic quality adjustment based on device capabilities