Avalonia is designed for high performance, but understanding how to optimize your application can make a significant difference in responsiveness and resource usage.
The Rendering Pipeline
Understanding the rendering pipeline helps identify bottlenecks:
- Property Changes - UI properties modified
- Layout Pass - Measure and Arrange
- Render Pass - Generate drawing instructions
- Composition - Combine visual layers
- Rasterization - Convert to pixels (GPU or CPU)
- Display - Present to screen
GPU-accelerated operations bypass most of the pipeline:
- Opacity changes
- RenderTransform modifications
- Clip changes (within bounds)
These can animate at 60+ FPS with minimal CPU overhead.
Layout Optimization
Minimize Layout Invalidation
Avoid animating these properties as they trigger full layout passes:
Width, Height
Margin, Padding
FontSize
HorizontalAlignment, VerticalAlignment
Use RenderTransform instead for visual effects.
Bad: Animating Width
<!-- Triggers layout on every frame -->
<Border>
<Border.Styles>
<Style Selector="Border">
<Style.Animations>
<Animation Duration="0:0:1">
<KeyFrame Cue="0%"><Setter Property="Width" Value="100"/></KeyFrame>
<KeyFrame Cue="100%"><Setter Property="Width" Value="200"/></KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Border.Styles>
</Border>
<!-- GPU-accelerated, no layout -->
<Border Width="100" RenderTransformOrigin="0, 0.5">
<Border.Styles>
<Style Selector="Border">
<Style.Animations>
<Animation Duration="0:0:1">
<KeyFrame Cue="0%">
<Setter Property="(Border.RenderTransform).(ScaleTransform.ScaleX)" Value="1"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="(Border.RenderTransform).(ScaleTransform.ScaleX)" Value="2"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Border.Styles>
<Border.RenderTransform>
<ScaleTransform />
</Border.RenderTransform>
</Border>
Choose the Right Panel
Different panels have different performance characteristics:
<!-- Fastest: Fixed positioning, no measurement -->
<Canvas>
<Button Canvas.Left="10" Canvas.Top="10" />
<Button Canvas.Left="120" Canvas.Top="10" />
</Canvas>
<!-- Fast: Simple single-direction layout -->
<StackPanel>
<Button />
<Button />
</StackPanel>
<!-- Moderate: Two-pass layout (rows/columns) -->
<Grid RowDefinitions="Auto,*" ColumnDefinitions="*,*">
<Button Grid.Row="0" Grid.Column="0" />
<Button Grid.Row="0" Grid.Column="1" />
</Grid>
<!-- Complex: Constraint-based layout -->
<RelativePanel>
<Button Name="Btn1" RelativePanel.AlignLeftWithPanel="True" />
<Button RelativePanel.RightOf="Btn1" />
</RelativePanel>
Performance ranking (fastest to slowest):
Canvas - O(n) fixed positioning
StackPanel - O(n) single-direction
DockPanel - O(n) with priority ordering
Grid - O(n×m) for n rows and m columns
RelativePanel - O(n²) constraint solving
WrapPanel - O(n) with reflow
Virtualization
For large collections, use virtualizing panels:
<!-- Virtualizes items, only creates visible ones -->
<ListBox ItemsSource="{Binding LargeCollection}"
VirtualizationMode="Simple">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
Virtualization modes:
Simple - Basic recycling of containers
Standard - Enhanced recycling with better performance
None - No virtualization (all items created)
When to virtualize:
- Collections with 100+ items
- Items with complex templates
- Scrollable content
- Data-heavy applications
Rendering Optimization
Cache Rendering
Use RenderTargetBitmap to cache expensive rendering:
using Avalonia.Media.Imaging;
public class ComplexControl : UserControl
{
private RenderTargetBitmap? _cachedBitmap;
private bool _isDirty = true;
public override void Render(DrawingContext context)
{
if (_isDirty || _cachedBitmap == null)
{
var size = new PixelSize(
(int)Bounds.Width,
(int)Bounds.Height);
_cachedBitmap?.Dispose();
_cachedBitmap = new RenderTargetBitmap(size);
using (var drawingContext = _cachedBitmap.CreateDrawingContext(null))
{
RenderComplex(drawingContext);
}
_isDirty = false;
}
context.DrawImage(_cachedBitmap,
new Rect(0, 0, Bounds.Width, Bounds.Height));
}
private void RenderComplex(DrawingContext context)
{
// Expensive rendering operations
}
private void InvalidateCache()
{
_isDirty = true;
InvalidateVisual();
}
}
Opacity Mask Optimization
<!-- Slow: Creates new brush on every render -->
<Border>
<Border.OpacityMask>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="Transparent" Offset="0" />
<GradientStop Color="White" Offset="1" />
</LinearGradientBrush>
</Border.OpacityMask>
</Border>
<!-- Fast: Reuses brush from resources -->
<Window.Resources>
<LinearGradientBrush x:Key="FadeBrush" StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="Transparent" Offset="0" />
<GradientStop Color="White" Offset="1" />
</LinearGradientBrush>
</Window.Resources>
<Border OpacityMask="{StaticResource FadeBrush}" />
<!-- Fast: Simple rectangular clip -->
<Border ClipToBounds="True">
<Image Source="large.png" />
</Border>
<!-- Slower: Complex geometry clip -->
<Border>
<Border.Clip>
<EllipseGeometry Rect="0,0,100,100" />
</Border.Clip>
<Image Source="large.png" />
</Border>
<!-- Slowest: Animated complex clip -->
<Border>
<Border.Clip>
<PathGeometry Figures="M 0,0 L 100,100 ..." />
</Border.Clip>
</Border>
Compiled Bindings
Use compiled bindings for better performance:
<Window xmlns:local="clr-namespace:MyApp"
x:DataType="local:MainViewModel">
<!-- Compiled binding: Fast -->
<TextBlock Text="{Binding UserName}" />
<!-- Reflection binding: Slower -->
<TextBlock Text="{Binding Path=UserName}" />
</Window>
Performance difference:
- Compiled bindings: Direct property access
- Reflection bindings: Runtime type inspection
Compiled bindings are automatically enabled when:
x:DataType is specified
- The property path is simple (no indexers or casts)
- The binding mode is OneWay or TwoWay
Observable Collections
using System.Collections.ObjectModel;
using System.Collections.Specialized;
// Good: Efficient incremental updates
public ObservableCollection<Item> Items { get; } = new();
public void AddItem(Item item)
{
Items.Add(item); // Notifies only about addition
}
// Bad: Replaces entire collection
public void AddItemBad(Item item)
{
var newList = Items.ToList();
newList.Add(item);
Items = new ObservableCollection<Item>(newList); // Full rebind!
}
Binding Updates
using ReactiveUI;
public class ViewModel : ReactiveObject
{
private string _name;
// Good: Only raises when value changes
public string Name
{
get => _name;
set => this.RaiseAndSetIfChanged(ref _name, value);
}
// Bad: Always raises, even if value hasn't changed
public string NameBad
{
get => _name;
set
{
_name = value;
this.RaisePropertyChanged();
}
}
}
Memory Management
Dispose Resources
public class MyControl : UserControl, IDisposable
{
private IDisposable? _subscription;
private RenderTargetBitmap? _bitmap;
public MyControl()
{
_subscription = Observable
.Interval(TimeSpan.FromSeconds(1))
.Subscribe(Update);
}
public void Dispose()
{
_subscription?.Dispose();
_bitmap?.Dispose();
}
protected override void OnDetachedFromVisualTree(
VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
Dispose();
}
}
Weak Event Patterns
using System;
// Bad: Creates memory leak
public class LeakyViewModel
{
public LeakyViewModel(IDataService service)
{
service.DataChanged += OnDataChanged;
// If service outlives ViewModel, ViewModel won't be GC'd
}
private void OnDataChanged(object sender, EventArgs e) { }
}
// Good: Uses weak events
public class SafeViewModel : IDisposable
{
private readonly IDataService _service;
public SafeViewModel(IDataService service)
{
_service = service;
WeakEventManager<IDataService, EventArgs>
.AddHandler(_service, nameof(service.DataChanged), OnDataChanged);
}
private void OnDataChanged(object sender, EventArgs e) { }
public void Dispose()
{
WeakEventManager<IDataService, EventArgs>
.RemoveHandler(_service, nameof(_service.DataChanged), OnDataChanged);
}
}
Image Management
using Avalonia.Media.Imaging;
// Good: Load once, reuse
private static readonly Bitmap _cachedIcon =
new Bitmap(AssetLoader.Open(new Uri("avares://App/icon.png")));
public Bitmap Icon => _cachedIcon;
// Bad: Loads image every time
public Bitmap IconBad =>
new Bitmap(AssetLoader.Open(new Uri("avares://App/icon.png")));
Thread Management
UI Thread vs Background Thread
using Avalonia.Threading;
public async Task LoadDataAsync()
{
// Bad: Blocks UI thread
var data = ExpensiveOperation();
Items.Clear();
foreach (var item in data)
Items.Add(item);
}
public async Task LoadDataGoodAsync()
{
// Good: Runs on background thread
var data = await Task.Run(() => ExpensiveOperation());
// Update UI on UI thread
await Dispatcher.UIThread.InvokeAsync(() =>
{
Items.Clear();
foreach (var item in data)
Items.Add(item);
});
}
public async Task LoadDataBestAsync()
{
// Best: Batch updates
var data = await Task.Run(() => ExpensiveOperation());
await Dispatcher.UIThread.InvokeAsync(() =>
{
Items.Clear();
Items.AddRange(data); // Single collection change
});
}
Async Operations
// Bad: Blocks with .Result
public void LoadData()
{
var data = FetchDataAsync().Result; // Blocks UI thread!
UpdateUI(data);
}
// Good: Async all the way
public async Task LoadDataAsync()
{
var data = await FetchDataAsync();
UpdateUI(data);
}
Profiling and Measurement
Enable Diagnostics
using Avalonia.Rendering;
protected override void OnStartup()
{
// Enable FPS counter
RendererDiagnostics.DebugOverlays =
RendererDebugOverlays.Fps |
RendererDebugOverlays.LayoutTimeGraph |
RendererDebugOverlays.RenderTimeGraph;
}
using System.Diagnostics;
public void MeasureOperation()
{
var sw = Stopwatch.StartNew();
ExpensiveOperation();
sw.Stop();
Debug.WriteLine($"Operation took {sw.ElapsedMilliseconds}ms");
}
public async Task MeasureLayoutAsync()
{
var control = new MyControl();
var sw = Stopwatch.StartNew();
control.Measure(new Size(1000, 1000));
control.Arrange(new Rect(0, 0, 1000, 1000));
sw.Stop();
Debug.WriteLine($"Layout took {sw.ElapsedMilliseconds}ms");
}
Memory Profiling
using System;
public void ProfileMemory()
{
var before = GC.GetTotalMemory(true);
// Operation to profile
var items = new List<Item>();
for (int i = 0; i < 10000; i++)
items.Add(new Item());
var after = GC.GetTotalMemory(false);
var allocated = (after - before) / 1024 / 1024;
Debug.WriteLine($"Allocated {allocated:F2} MB");
}
GPU Acceleration
using Avalonia.Skia;
// Configure GPU memory limits
AppBuilder.Configure<App>()
.UsePlatformDetect()
.With(new SkiaOptions
{
MaxGpuResourceSizeBytes = 256 * 1024 * 1024 // 256 MB
})
.StartWithClassicDesktopLifetime(args);
Render Scaling
// Get current DPI scaling
var window = (Window)this.GetVisualRoot();
var scaling = window.RenderScaling;
// Create appropriately sized bitmaps
var pixelSize = new PixelSize(
(int)(logicalWidth * scaling),
(int)(logicalHeight * scaling));
Before releasing:✓ Profile with Release build (not Debug)
✓ Test with realistic data volumes
✓ Check memory usage over time
✓ Verify smooth scrolling (60 FPS)
✓ Test on minimum spec hardware
✓ Monitor startup time
✓ Check for memory leaks
✓ Verify responsive UI during operations
Common performance issues:✗ Animating layout properties
✗ Not virtualizing large lists
✗ Creating controls in loops
✗ Binding to complex expressions
✗ Loading resources repeatedly
✗ Synchronous file I/O on UI thread
✗ Not disposing bitmaps and subscriptions
✗ Over-complicated visual trees
See Also