Performance Optimization
FlowGraph is designed to scale from simple diagrams to complex graphs with thousands of nodes. This guide shows you how to configure FlowGraph for optimal performance at any scale.
How does FlowGraph compare? Most web-based graph editors (React Flow, JointJS) recommend limiting graphs to 500 nodes. FlowGraph's architecture supports 2000+ nodes with smooth interactions when properly configured. The techniques in this guide unlock that scale.
Quick Start: Optimization Presets
For most use cases, start with these presets and adjust as needed:
// Preset for general-purpose graphs (works well up to ~500 nodes)
public static FlowCanvasSettings Balanced() => new()
{
EnableVirtualization = true,
VirtualizationBuffer = 200,
UseSimplifiedNodeRendering = false,
AutoRouteEdges = true,
RouteOnlyAffectedEdges = true
};
// Preset for maximum performance (2000+ nodes)
public static FlowCanvasSettings HighPerformance() => new()
{
EnableVirtualization = true,
VirtualizationBuffer = 100,
UseSimplifiedNodeRendering = true,
DirectRenderingNodeThreshold = 100,
AutoRouteEdges = false
};
Virtualization
Virtualization renders only visible nodes and edges, significantly improving performance for large graphs.
// Enable virtualization (enabled by default)
canvas.Settings.EnableVirtualization = true;
// Configure buffer zone (nodes within this distance from viewport are rendered)
canvas.Settings.VirtualizationBuffer = 200; // Canvas units
// Virtualization is recommended for graphs with 100+ nodes
When to Use Virtualization
Virtualization is the single most impactful optimization. It ensures only visible elements consume rendering resources.
| Graph Size | Recommendation | Why |
|---|---|---|
| < 100 nodes | Optional | Overhead may not be worth it |
| 100-500 nodes | Recommended | Noticeable improvement |
| 500+ nodes | Essential | Critical for smooth interactions |
Pro tip: Set
VirtualizationBufferbased on your node sizes. Larger nodes need larger buffers to avoid pop-in during fast panning.
Simplified Node Rendering
For graphs with hundreds of nodes, use simplified rendering to reduce visual tree complexity:
// Enable simplified rendering
canvas.EnableSimplifiedRendering();
// This replaces node renderers with minimal visual elements
// - Single Border + TextBlock per node
// - No shadows, gradients, or complex effects
// - Significantly faster rendering
// Disable when needed
canvas.DisableSimplifiedRendering();
// Or configure via settings
canvas.Settings.UseSimplifiedNodeRendering = true;
canvas.Settings.RenderBatchSize = 50; // Render in batches to keep UI responsive
Direct GPU Rendering
For very large graphs, FlowGraph can bypass the Avalonia visual tree and render directly to the GPU. This is how professional diagramming tools achieve smooth performance with thousands of elements.
// Enable direct rendering
canvas.EnableDirectRendering();
// This mode:
// - Draws nodes/edges directly to DrawingContext
// - Bypasses visual tree overhead
// - Trades some interactivity for performance
// - Ideal for 500+ nodes
// Automatically enables at threshold
canvas.Settings.DirectRenderingNodeThreshold = 100;
// Disable direct rendering
canvas.DisableDirectRendering();
What You Keep vs. What Changes
| Feature | Standard Mode | Direct Rendering |
|---|---|---|
| Pan/Zoom | ✅ Full | ✅ Full |
| Node dragging | ✅ Full | ✅ Full |
| Edge connections | ✅ Full | ✅ Full |
| Selection | ✅ Full | ✅ Full |
| Hover effects | ✅ Animated | ⚡ Simplified |
| Custom controls in nodes | ✅ Full | ❌ Not supported |
| Animations | ✅ Full | ⚡ Basic |
When to use: Enable direct rendering when you need smooth interactions with 500+ nodes and don't require embedded controls (buttons, text inputs) inside nodes. Most workflow and diagram applications work perfectly in this mode.
Spatial Indexing (Quadtree)
FlowGraph uses a Quadtree spatial index for O(log N) hit testing performance. This is automatically enabled and provides significant improvements for graphs with 100+ elements.
How It Works
Instead of checking every node/shape when you click or hover, the Quadtree partitions the canvas into regions, allowing FlowGraph to quickly narrow down which elements are near the cursor.
| Graph Size | Linear Search | Quadtree |
|---|---|---|
| 100 elements | ~100 checks | ~7 checks |
| 1000 elements | ~1000 checks | ~10 checks |
| 10000 elements | ~10000 checks | ~13 checks |
Benefits
- Faster hit testing - Click and hover detection scales logarithmically
- Smoother interactions - Less CPU work during mouse movement
- Automatic - No configuration needed, works out of the box
The spatial index automatically updates when nodes are added, removed, or moved.
Batch Loading
When initially loading a large graph, use batch operations to suppress individual change notifications:
using FlowGraph.Core;
// Method 1: Batch load mode
graph.BeginBatchLoad();
try
{
for (int i = 0; i < 10000; i++)
{
graph.AddNode(CreateNode(i));
}
for (int i = 0; i < 20000; i++)
{
graph.AddEdge(CreateEdge(i));
}
}
finally
{
graph.EndBatchLoad(); // Single notification at end
}
// Method 2: Bulk operations
var nodes = Enumerable.Range(0, 10000).Select(CreateNode);
var edges = Enumerable.Range(0, 20000).Select(CreateEdge);
graph.AddNodes(nodes); // Single notification
graph.AddEdges(edges); // Single notification
Edge Routing Performance
Edge routing (calculating paths that avoid overlapping nodes) is computationally intensive. Here's how to optimize it without losing functionality:
// Strategy 1: Defer routing during load
canvas.Settings.AutoRouteEdges = false;
LoadLargeGraph();
canvas.Settings.AutoRouteEdges = true;
canvas.Routing.RouteAllEdges(); // Single batch operation
// Strategy 2: Route only what changed
canvas.Settings.RouteOnlyAffectedEdges = true; // Only re-route edges connected to moved nodes
// Strategy 3: Disable re-routing during drag (route on drop)
canvas.Settings.RouteEdgesOnDrag = false;
// Strategy 4: Use simpler routing for large graphs
canvas.Settings.EdgeRoutingAlgorithm = RouterAlgorithm.Direct; // Straight lines
// or
canvas.Settings.EdgeRoutingAlgorithm = RouterAlgorithm.Bezier; // Simple curves
// vs.
canvas.Settings.EdgeRoutingAlgorithm = RouterAlgorithm.SmartBezier; // Obstacle avoidance (slower)
Routing Algorithm Comparison
| Algorithm | Speed | Visual Quality | Node Avoidance |
|---|---|---|---|
| Direct | ⚡⚡⚡ Fastest | Basic | No |
| Bezier | ⚡⚡ Fast | Good | No |
| Orthogonal | ⚡ Medium | Clean | Yes |
| SmartBezier | Slower | Best | Yes |
Recommendation: Use
SmartBezierfor graphs under 200 edges where visual clarity matters. UseBezierorDirectfor larger graphs.
Recommended Settings by Graph Size
These configurations have been tested for optimal balance of features and performance.
Small Graphs (< 100 nodes)
Full features, no compromises needed.
var settings = new FlowCanvasSettings
{
EnableVirtualization = false, // Not needed
UseSimplifiedNodeRendering = false,
DirectRenderingNodeThreshold = 0, // Disabled
AutoRouteEdges = true,
RouteEdgesOnDrag = true,
EdgeRoutingAlgorithm = RouterAlgorithm.SmartBezier
};
Medium Graphs (100-500 nodes)
Enable virtualization, keep full interactivity.
var settings = new FlowCanvasSettings
{
EnableVirtualization = true,
VirtualizationBuffer = 200,
UseSimplifiedNodeRendering = false,
DirectRenderingNodeThreshold = 300, // Auto-enable if needed
AutoRouteEdges = true,
RouteEdgesOnDrag = true,
RouteOnlyAffectedEdges = true,
EdgeRoutingAlgorithm = RouterAlgorithm.Bezier
};
Large Graphs (500-2000 nodes)
Optimized rendering, selective routing.
var settings = new FlowCanvasSettings
{
EnableVirtualization = true,
VirtualizationBuffer = 150,
UseSimplifiedNodeRendering = true,
RenderBatchSize = 50,
DirectRenderingNodeThreshold = 100,
AutoRouteEdges = true,
RouteEdgesOnDrag = false, // Route on drop instead
RouteOnlyAffectedEdges = true,
EdgeRoutingAlgorithm = RouterAlgorithm.Bezier
};
canvas.Settings = settings;
canvas.EnableSimplifiedRendering();
Very Large Graphs (2000+ nodes)
Maximum performance mode. Full pan/zoom/selection, simplified visuals.
var settings = new FlowCanvasSettings
{
EnableVirtualization = true,
VirtualizationBuffer = 100,
UseSimplifiedNodeRendering = true,
RenderBatchSize = 100,
DirectRenderingNodeThreshold = 50,
AutoRouteEdges = false, // On-demand routing
RouteEdgesOnDrag = false,
EdgeRoutingAlgorithm = RouterAlgorithm.Direct
};
canvas.Settings = settings;
canvas.EnableDirectRendering();
// Route edges on-demand when user selects nodes
canvas.SelectionChanged += (s, e) =>
{
if (e.AddedNodes.Any())
canvas.Routing.RouteEdgesForNodes(e.AddedNodes);
};
Performance Monitoring
Use the FlowDiagnostics control to monitor performance:
<fgc:FlowPanel Position="TopLeft" Margin="8">
<fgc:FlowDiagnostics TargetCanvas="{Binding #Canvas}" />
</fgc:FlowPanel>
The diagnostics panel shows:
- Node count (visible/total)
- Edge count (visible/total)
- Render time
- Frame rate
- Viewport info
- Current input state
Best Practices
Loading Large Graphs
Use batch operations - Always use
AddNodes()/AddEdges()instead of loops with individualAddNode()/AddEdge()callsDisable animations during load - Prevents rendering overhead during initial population
Defer routing - Disable
AutoRouteEdgesduring load, enable after
// Optimized loading pattern
public async Task LoadGraphOptimized(IEnumerable<Node> nodes, IEnumerable<Edge> edges)
{
// Prepare canvas
canvas.Settings.AutoRouteEdges = false;
canvas.BeginBatchUpdate();
try
{
// Batch add - triggers single notification
graph.AddNodes(nodes);
graph.AddEdges(edges);
}
finally
{
canvas.EndBatchUpdate();
}
// Fit view and route
canvas.FitToView();
canvas.Settings.AutoRouteEdges = true;
}
Custom Node Renderers
If you're creating custom node renderers, keep them lightweight:
// ❌ Avoid: Complex visual tree
public override Control CreateVisual(Node node)
{
return new Border
{
Child = new StackPanel
{
Children =
{
new Border { Child = new TextBlock { /* header */ } },
new ItemsControl { /* ports */ },
new Border { Child = new ContentPresenter { /* content */ } }
}
}
};
}
// ✅ Better: Flat structure
public override Control CreateVisual(Node node)
{
return new NodeControl(node); // Single custom control
}
// ✅ Best: Direct rendering
public override void Render(DrawingContext context, Node node)
{
// Draw directly - no visual tree overhead
context.DrawRectangle(brush, pen, bounds);
context.DrawText(formattedText, position);
}
Data Management
Store large payloads outside the graph:
// ❌ Avoid: Large data in node
node.Data = new {
Image = LoadBitmap(), // Large!
Document = LoadXml(), // Large!
Metadata = complexObject // Serialization overhead
};
// ✅ Better: Reference by ID
node.Data = new NodeData {
ImageId = "img-123",
DocumentId = "doc-456"
};
// Load on demand
var image = await imageCache.GetAsync(nodeData.ImageId);
Profiling Tips
- Profile first - Use FlowDiagnostics to identify actual bottlenecks before optimizing
- Check visible counts - If visible nodes << total nodes, virtualization is working
- Monitor render time - Should be < 16ms for 60fps
- Watch memory - Growing memory indicates caching issues
Async Loading Example
public async Task LoadLargeGraphAsync(string filePath)
{
// Disable rendering updates
canvas.Settings.AutoRouteEdges = false;
// Load graph data
var graph = await GraphExtensions.LoadFromFileAsync(filePath);
// Show progress
var progress = new Progress<int>(value =>
{
ProgressBar.Value = value;
});
// Batch load into canvas
graph.BeginBatchLoad();
// Could add nodes in chunks here if needed
// for better responsiveness
graph.EndBatchLoad();
// Set graph
canvas.Graph = graph;
// Fit to view after load
await Task.Delay(100); // Allow layout
canvas.FitToView();
// Re-enable routing if needed
canvas.Settings.AutoRouteEdges = true;
}
Memory Management
For long-running applications:
// Clear graph when switching documents
canvas.Graph = null;
// Force garbage collection if needed
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
// Dispose of custom resources in node Data property
foreach (var node in graph.Nodes)
{
if (node.Data is IDisposable disposable)
disposable.Dispose();
}
Summary
FlowGraph scales from small diagrams to enterprise-grade graphs with thousands of nodes. The key is matching your configuration to your graph size.
| Graph Size | Virtualization | Simplified Rendering | Direct Rendering | Edge Routing |
|---|---|---|---|---|
| < 100 | Optional | No | No | Full (SmartBezier) |
| 100-500 | Yes | Optional | Optional | Full (Bezier) |
| 500-2000 | Yes | Recommended | Recommended | Selective |
| 2000+ | Yes | Yes | Yes | On-Demand |
Remember: These are recommendations, not limitations. FlowGraph's architecture supports all configurations—you choose the balance of features and performance that fits your application.
Common Scenarios
| Use Case | Recommended Config | Why |
|---|---|---|
| Node-based editor (Blender-style) | Balanced | Need interactive nodes with controls |
| Workflow designer | Balanced | Moderate size, full features |
| Network topology viewer | High Performance | Large graphs, read-mostly |
| Data lineage visualization | High Performance | Thousands of nodes, minimal editing |
| Mind mapping | Balanced | Interactive, moderate size |