Skip to main content
Avalonia is designed for high performance, but understanding how to optimize your application can make a significant difference in responsiveness and resource usage.

Performance Fundamentals

The Rendering Pipeline

Understanding the rendering pipeline helps identify bottlenecks:
  1. Property Changes - UI properties modified
  2. Layout Pass - Measure and Arrange
  3. Render Pass - Generate drawing instructions
  4. Composition - Combine visual layers
  5. Rasterization - Convert to pixels (GPU or CPU)
  6. 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>

Good: Using RenderTransform

<!-- 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):
  1. Canvas - O(n) fixed positioning
  2. StackPanel - O(n) single-direction
  3. DockPanel - O(n) with priority ordering
  4. Grid - O(n×m) for n rows and m columns
  5. RelativePanel - O(n²) constraint solving
  6. 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}" />

Clipping Performance

<!-- 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>

Data Binding Performance

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:
  1. x:DataType is specified
  2. The property path is simple (no indexers or casts)
  3. 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;
}

Measure Performance

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");
}

Platform-Specific Optimizations

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));

Performance Checklist

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

Build docs developers (and LLMs) love