Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 71 additions & 62 deletions docs/app-development/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,35 @@ This guide covers common performance considerations for Avalonia applications an

## UI virtualization

When displaying large collections, virtualization ensures only visible items are created and rendered. Avalonia's `ListBox`, `TreeView`, `DataGrid`, and `ItemsRepeater` support virtualization by default.
When displaying large collections, virtualization ensures only visible items are created and rendered. Some controls support virtualization by default, such as [`ListBox`](/controls/data-display/collections/listbox) or [`ItemsRepeater`](/controls/data-display/collections/itemsrepeater).

### How virtualization works

Instead of creating a control for every item in the collection, the virtualizing panel creates controls only for visible items. As the user scrolls, controls that move off-screen are recycled and reused for new items coming into view.

### Ensuring virtualization is active

Virtualization requires a constrained height. If the items control is inside a `StackPanel` or another control that gives it infinite height, virtualization is disabled:
Virtualization requires a constrained height. If the item is inside a control that gives it infinite height, virtualization is disabled.
Comment thread
luke-whos-here marked this conversation as resolved.

```xml
<!-- BAD: StackPanel gives infinite height, disabling virtualization -->
<!-- DON'T: StackPanel gives infinite height, disabling virtualization -->
<StackPanel>
<ListBox ItemsSource="{Binding LargeCollection}" />
</StackPanel>

<!-- GOOD: Grid row with * constrains height -->
<!-- DO: Grid row with * constrains height -->
<Grid RowDefinitions="*">
<ListBox ItemsSource="{Binding LargeCollection}" />
</Grid>

<!-- GOOD: DockPanel fill area constrains height -->
<!-- DO: DockPanel fill area constrains height -->
<DockPanel>
<TextBlock DockPanel.Dock="Top" Text="Items" />
<ListBox ItemsSource="{Binding LargeCollection}" />
</DockPanel>
```

### ItemsRepeater for custom layouts
### `ItemsRepeater` for custom layouts

`ItemsRepeater` provides a lower-level virtualizing control for custom layouts:

Expand All @@ -58,7 +58,7 @@ Virtualization requires a constrained height. If the items control is inside a `

### Buffer factor for smooth scrolling

`VirtualizingStackPanel` supports a `BufferFactor` property that keeps additional items realized beyond the visible viewport. This reduces recycling frequency during scrolling, which can eliminate stutter caused by garbage collection, particularly on mobile devices.
`VirtualizingStackPanel` supports a `BufferFactor` property that keeps additional items beyond the visible viewport in a realized state. This reduces recycling frequency during scrolling, which can eliminate stutter, particularly on mobile devices.

```xml
<ListBox ItemsSource="{Binding LargeCollection}">
Expand All @@ -70,21 +70,21 @@ Virtualization requires a constrained height. If the items control is inside a `
</ListBox>
```

A `BufferFactor` of `1` realizes items across one extra viewport height above and below the visible area. The default is `0` (no buffer). Higher values use more memory but produce smoother scrolling for complex item templates.
A `BufferFactor` of `1` realizes items across one extra viewport height above and below the visible area. The default is `0` (no buffer). Higher values use more memory but produce smoother scrolling.

### Variable-height items

`VirtualizingStackPanel` works best when all items have the same height. When items have variable heights, the panel must estimate total scroll extent based on measured items, which can cause scroll bar jumps and layout recalculations. If your items vary significantly in height, consider these strategies:
`VirtualizingStackPanel` is optimized for collections where all items have the same height. The panel estimates scroll extent based on number of items, meaning collections containing items with variable heights can cause scroll bar jumps and layout recalculations. If your items vary significantly in height, consider these strategies:

- **Use a uniform estimated height.** Give all items a fixed `Height` or `MinHeight` so the virtualizing panel can calculate scroll extent accurately. Allow content to clip or scroll internally if it exceeds the estimated size.
- **Flatten hierarchical data.** Instead of nesting expanders inside a virtualizing list, flatten the tree into a single list with indent levels. This lets the virtualizing panel manage all rows directly. `TreeView` uses this approach internally.
- **Use a uniform height.** Give all items a fixed `Height` or `MinHeight` so the virtualizing panel can calculate scroll extent accurately. Allow content to clip or scroll internally if it exceeds the estimated size.
- **Flatten hierarchical data.** Instead of nesting expanders inside a virtualizing list, flatten the tree into a single list with indent levels. This lets the virtualizing panel manage rows directly. `TreeView` uses this approach internally.
- **Limit realized items.** If virtualization is not feasible (for example, a complex property grid with expanders), limit how many controls exist at once. Load only the visible section and create additional items on demand as the user expands or scrolls.

### Reduce control template complexity
### Reducing control template complexity

Complex controls like [`TextBox`](/api/avalonia/controls/textbox) contain a deep visual tree (borders, scroll viewers, watermark layers). When you create thousands of them, template instantiation and measurement dominate startup time.
Complex controls like [`TextBox`](/controls/input/text-input/textbox) contain a deep visual tree with borders, scroll viewers and watermark layers. When you create many of them, template instantiation and measurement dominate startup time.

**Use lightweight controls for display, swap on interaction.** Show values with `TextBlock` (which has a minimal visual tree) and replace with a `TextBox` only when the user clicks to edit:
**Use lightweight controls for display and swap on interaction.** For example, you can show values with `TextBlock` (which has a minimal visual tree) and replace it with a `TextBox` only when the user clicks to edit:

```csharp
// In your DataTemplate code-behind or custom control
Expand All @@ -103,7 +103,7 @@ display.PointerPressed += (s, e) =>
};
```

**Re-template heavy controls.** If you must use `TextBox` everywhere, create a simplified control theme that removes unnecessary visual elements (watermark, clear button, scroll viewer) to reduce the visual tree depth:
**Re-template heavy controls.** If you must use `TextBox` everywhere, create a simplified control theme that removes unnecessary visual elements (e.g., watermark, clear button, scroll viewer) to reduce the visual tree depth:

```xml
<ControlTheme x:Key="LightTextBox" TargetType="TextBox">
Expand All @@ -127,9 +127,9 @@ Apply it to controls that do not need the full feature set:
<TextBox Theme="{StaticResource LightTextBox}" Text="{Binding Value}" />
```

## Layout performance
## Layout optimization

### Avoid deep nesting
### Avoiding deep nesting

Each level of nesting adds measure and arrange passes. Flatten your layout where possible:

Expand All @@ -151,7 +151,7 @@ Each level of nesting adds measure and arrange passes. Flatten your layout where
</StackPanel>
```

### Use Grid instead of nested StackPanels
### Replacing nested stack panels with grids

A single `Grid` with rows and columns is more efficient than multiple nested `StackPanel` controls:

Expand All @@ -165,9 +165,9 @@ A single `Grid` with rows and columns is more efficient than multiple nested `St
</Grid>
```

### Minimize InvalidateArrange / InvalidateMeasure
### Minimizing `InvalidateArrange` and `InvalidateMeasure`

Property changes that affect layout (e.g., Width, Height, Margin, Padding) trigger layout recalculations. Batch property changes when possible:
Property changes that affect layout (e.g., `Width`, `Height`, `Margin`, `Padding`) trigger layout recalculations. Batch property changes when possible:

```csharp
// Set multiple properties together; Avalonia batches layout
Expand All @@ -178,9 +178,9 @@ myControl2.Height = 200;

## Rendering performance

### Hide unused controls with IsVisible
### Hiding unused controls with `IsVisible`

Setting `IsVisible="False"` completely removes a control from both layout and rendering. The layout system skips the measure and arrange passes for that control and its entire subtree, and the renderer does not draw it. This makes `IsVisible` an effective way to reduce work for conditionally shown content:
Setting `IsVisible="False"` removes a control from both layout and rendering. The layout system skips the measure and arrange passes for that control and its entire subtree, and the renderer does not draw it. This makes `IsVisible` an effective way to reduce work for conditionally shown content:

```xml
<Panel>
Expand All @@ -192,29 +192,31 @@ Setting `IsVisible="False"` completely removes a control from both layout and re

If you need to hide a control visually while keeping its layout space reserved, use `Opacity="0"` instead. An element with `Opacity="0"` still participates in layout and can receive input.

### Use ClipToBounds judiciously
### Using `ClipToBounds` judiciously

`ClipToBounds="True"` creates a clip layer. Only use it when child content actually exceeds the control bounds.

### Reduce hit-testing cost
### Reducing hit-testing cost

When a pointer event occurs, Avalonia walks the visual tree and tests each element. With hundreds or thousands of children in a `Canvas` or `Panel`, this linear walk adds a noticeable delay between clicking and receiving the event. Set `IsHitTestVisible="False"` on elements that do not need pointer interaction, and consider an overlay-based hit-test strategy or custom rendering for scenes with many objects. See [Hit Testing: Performance with many elements](/docs/graphics-animation/hit-testing#performance-with-many-elements) for patterns and code examples.
When a pointer event occurs, Avalonia walks the visual tree and tests each element. This linear walk can cause a noticeable delay between clicking and receiving the event if a control contains many children.

Transparent elements also participate in hit testing. If a control does not need pointer interaction, set `IsHitTestVisible="False"`:
Set `IsHitTestVisible="False"` on elements that do not need pointer interaction. Consider an overlay-based hit-test strategy or custom rendering for scenes with many objects. See [Hit Testing: Performance with many elements](/docs/graphics-animation/hit-testing#performance-with-many-elements) for patterns and code examples.

Transparent elements also participate in hit testing. If a transparent control does not need pointer interaction, set `IsHitTestVisible="False"` to exclude it from hit-testing.

```xml
<Border Background="Transparent" IsHitTestVisible="False">
<!-- Overlay that should not capture clicks -->
</Border>
```

### Reduce visual complexity
### Reducing visual complexity

- Minimize the number of `BoxShadow` effects (each shadow adds a render pass)
- Avoid overlapping semi-transparent elements
- Use `Opacity` on a parent element rather than on each child individually
- Minimize the number of `BoxShadow` effects, which add an individual render pass each.
- Avoid overlapping semi-transparent elements.
- Use `Opacity` on a parent element rather than on each child.

### BitmapCache
### Bitmap cache

For visuals that are expensive to render but change infrequently, use `BitmapCache` to rasterize them to a bitmap surface. The control and its children are rendered once into an intermediate bitmap, and that bitmap is reused for subsequent frames until the content changes.

Expand All @@ -227,15 +229,15 @@ For visuals that are expensive to render but change infrequently, use `BitmapCac
</Border>
```

`BitmapCache` properties:
#### `BitmapCache` properties

| Property | Type | Default | Description |
|---|---|---|---|
| `RenderAtScale` | `double` | `1` | Resolution multiplier for the cached bitmap. Values above 1 increase quality (useful for content that will be scaled up), values below 1 reduce memory at the cost of quality. Set to 0 to disable caching. |
| `RenderAtScale` | `double` | `1` | Resolution multiplier for the cached bitmap. Values above 1 increase quality. Values below 1 reduce memory at the cost of quality. A value of 0 disables caching. |
| `SnapsToDevicePixels` | `bool` | `false` | Aligns the cached bitmap to device pixel boundaries for sharper text and line rendering. |
| `EnableClearType` | `bool` | `false` | Enables ClearType subpixel text rendering within the cached surface. Without this, text in the cache uses grayscale antialiasing. |
| `EnableClearType` | `bool` | `false` | Enables `ClearType` subpixel text rendering within the cached surface. Without this, text in the cache uses grayscale antialiasing. |

For best results with text-heavy cached content, enable both `SnapsToDevicePixels` and `EnableClearType`:
For text-heavy content, it is recommended to cache with `SnapsToDevicePixels` and `EnableClearType` enabled.

```xml
<Border>
Expand All @@ -246,7 +248,7 @@ For best results with text-heavy cached content, enable both `SnapsToDevicePixel
</Border>
```

### BitmapInterpolationMode
### Bitmap interpolation mode

For images that do not need high-quality scaling, use a lower interpolation mode:

Expand All @@ -257,9 +259,9 @@ For images that do not need high-quality scaling, use a lower interpolation mode

### GPU resource cache size

Avalonia uses Skia with GPU acceleration by default. Skia maintains a GPU resource cache for textures and other GPU-backed surfaces. The default cache limit is approximately 28 MB. If your app works with large images, tilesets, or many cached visuals, images that exceed the cache limit are re-uploaded to the GPU each frame, causing stuttering.
Avalonia uses Skia with GPU acceleration by default. Skia maintains a GPU resource cache for textures and other GPU-backed surfaces. The default cache limit is approximately 28 MB. If your app works with large images, tilesets, or many cached visuals, images that exceed the cache limit are re-uploaded to the GPU each frame, which can cause stuttering.

Increase the cache by configuring `SkiaOptions` at startup:
Increase the cache limit by configuring `SkiaOptions` at startup:

```csharp
AppBuilder.Configure<App>()
Expand All @@ -272,25 +274,32 @@ AppBuilder.Configure<App>()

Choose a value appropriate for your target hardware. Most integrated GPUs have at least 2 GB of shared memory, so values of 256 MB or 512 MB are safe for desktop apps. Mobile devices may require lower values.

## Data binding performance
### Region dirty rect clipping

### Use compiled bindings
When content changes, Avalonia repaints the affected (or "dirty") regions of the screen rather than the whole frame. [`CompositionOptions.UseRegionDirtyRectClipping`](/api/avalonia/rendering/composition/compositionoptions) enables more accurate dirty-rect tracking by utilizing regions, but adds extra CPU time to process the render pass.

Compiled bindings resolve property paths at compile time, avoiding runtime reflection:
This option is **disabled by default** starting with Avalonia 12.1 to minimize loss of frame rate.

```xml
<UserControl x:CompileBindings="True" x:DataType="vm:MainViewModel">
<TextBlock Text="{Binding Name}" />
</UserControl>
To enable region clipping, you must explicitly set `UseRegionDirtyRectClipping = true` in `CompositionOptions` at startup. Enabling this option can be useful on some target platforms without GPU acceleration, such as [embedded Linux](/docs/platform-specific-guides/embedded-linux/embedded-linux) or other software-rendered devices, where reducing the painted area matters more than the clipping cost.

```csharp
AppBuilder.Configure<App>()
.UsePlatformDetect()
.With(new CompositionOptions
{
UseRegionDirtyRectClipping = true
});
```

Or enable project-wide in `.csproj`:
When region clipping is enabled, `MaxDirtyRects` caps how many dirty rects are tracked per frame. The default is `8`. Setting it to zero or a negative value bypasses Avalonia's tracking and uses the underlying drawing context's region support directly.

```xml
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
```
## Data binding performance

### Compiled bindings

Compiled bindings resolve property paths at compile time, avoiding runtime reflection. They are [enabled by default from Avalonia version 12](/docs/avalonia12-breaking-changes#compiled-bindings-are-enabled-by-default).

### Avoid unnecessary bindings
### Avoiding unnecessary bindings

Use static values instead of bindings for properties that never change:

Expand All @@ -303,21 +312,21 @@ Use static values instead of bindings for properties that never change:
<TextBlock Text="My Application" />
```

### Use OneTime bindings for static data
### Using one-time bindings for static data

If a value is set once and never changes, use `OneTime` mode to avoid ongoing change tracking:

```xml
<TextBlock Text="{Binding Version, Mode=OneTime}" />
```

## Collection performance
## Collections

### Use ObservableCollection for small-to-medium lists
### Using `ObservableCollection` for small-to-medium lists

`ObservableCollection<T>` notifies the UI of individual item additions and removals efficiently.

### Batch large updates
### Batching large updates

When adding many items at once, consider replacing the collection rather than adding items one by one:

Expand All @@ -333,7 +342,7 @@ OnPropertyChanged(nameof(Items));

### Incremental loading

When you must create many controls without virtualization (for example, a property grid or inspector panel), adding them all at once blocks the UI thread during measurement. Instead, add items in batches and yield to the dispatcher between each batch so the UI remains responsive:
If you must create many controls without virtualization (for example, a property grid or inspector panel), adding them all at once blocks the UI thread during measurement. Instead, add items in batches and yield to the dispatcher between each batch so the UI remains responsive:

```csharp
private async Task LoadItemsIncrementally(IList<ItemViewModel> items, Panel container)
Expand All @@ -354,15 +363,15 @@ private async Task LoadItemsIncrementally(IList<ItemViewModel> items, Panel cont
}
```

Choose a batch size large enough to fill the visible area on the first pass. This lets the user see content immediately while remaining items load progressively.
Choose a batch size large enough to fill the visible area on the first pass. This lets the user see content immediately, while the remaining items load progressively.

### Use DynamicData for large reactive collections
### Using `DynamicData` for large reactive collections

For collections with frequent sorting, filtering, or complex transformations, [DynamicData](https://github.com/reactivemarbles/DynamicData) provides optimized reactive pipelines that minimize UI updates.

## Async and threading

### Keep the UI thread free
### Keeping the UI thread free

Move heavy computation to background threads:

Expand All @@ -371,7 +380,7 @@ var data = await Task.Run(() => LoadLargeDataSet());
Items = new ObservableCollection<Item>(data);
```

### Debounce rapid input
### Debouncing rapid input

For search-as-you-type scenarios, debounce the input to avoid running expensive operations on every keystroke:

Expand All @@ -381,9 +390,9 @@ this.WhenAnyValue(x => x.SearchText)
.Subscribe(text => ApplyFilter(text));
```

### Use DispatcherPriority.Background for deferred work
### Using `DispatcherPriority.Background` for deferred work

Schedule low-priority updates that run when the UI thread is idle:
Schedule low-priority updates to run when the UI thread is idle:

```csharp
Dispatcher.UIThread.Post(() =>
Expand All @@ -405,7 +414,7 @@ JetBrains profiling tools work with Avalonia applications. Use them to identify

### Diagnostic overlays

Enable the FPS overlay in your `App.axaml.cs`:
Enable the FPS overlay in `App.axaml.cs`:

```csharp
public override void OnFrameworkInitializationCompleted()
Expand Down
Loading
Loading