TL;DR: Slow UI, janky scrolling, and rising memory usage in .NET MAUI apps often come from everyday design and binding decisions. This article breaks down the most common performance anti‑patterns affecting rendering, layout, collections, images, and profiling, so developers can recognize problems early and build smoother, more reliable MAUI apps.
If you’ve ever built a .NET MAUI app that works but doesn’t quite feel right, sluggish scrolling, delayed taps, or that awkward pause during navigation, you’re not alone.
And no, it’s usually not because “MAUI is slow.”
In most real-world apps, performance problems creep in quietly. They’re the result of everyday decisions we make while building UI, wiring up bindings, or loading data. Individually, they seem harmless. Together, they add up to an app that feels heavier than it should.
Let’s talk about the most common performance anti‑patterns I keep seeing in MAUI apps and, more importantly, how to think about avoiding them without turning your codebase into a science experiment.
1. When layouts get too deep for their own good
StackLayouts are comfortable. They’re easy to read, easy to tweak, and easy to overuse.
The trouble starts when a simple screen turns into a StackLayout inside another StackLayout, wrapped in yet another layout “just to align things nicely.” Every extra layer adds work for the layout engine, which measures, arranges, and recalculates during scrolling or screen transitions.
You’ll feel this most on mid‑range Android devices, where scrolling suddenly loses its smoothness.
In practice, flatter layouts win. A single Grid or FlexLayout often replaces three or four nested containers, keeps intent clear, and avoids unnecessary layout passes. Less nesting usually means fewer surprises later.
Example: Replace nested Stack Layouts with Grid
XAML
<!-- Poor: nested StackLayouts -->
<StackLayout>
<StackLayout Orientation="Horizontal">
<Label Text="Name"/>
<Entry x:Name="nameEntry"/>
</StackLayout>
<StackLayout Orientation="Horizontal">
<Label Text="Email"/>
<Entry x:Name="emailEntry"/>
</StackLayout>
</StackLayout>
<!-- Better: single Grid -->
<Grid ColumnDefinitions="Auto,*"
RowDefinitions="Auto,Auto">
<Label Grid.Row="0"
Grid.Column="0"
Text="Name"/>
<Entry Grid.Row="0"
Grid.Column="1"
x:Name="nameEntry"/>
<Label Grid.Row="1"
Grid.Column="0"
Text="Email"/>
<Entry Grid.Row="1"
Grid.Column="1"
x:Name="emailEntry"/>
</Grid>
2. ScrollView + CollectionView: A silent performance killer
This one shows up in production apps more often than it should.
Wrapping a CollectionView inside a ScrollView seems innocent until you realize you’ve just disabled virtualization. MAUI can no longer recycle item views efficiently, so it starts creating everything upfront.
On small lists, you won’t notice. On real data? Memory usage spikes, scrolling stutters, and the UI occasionally freezes just long enough for users to complain.
CollectionView is already designed to scroll. Let it do its job. If the page needs structure, size it using a Grid or layout container, not another scrolling surface.
Examples:
XAML
<!-- Bad: CollectionView in ScrollView -->
<ScrollView>
<CollectionView ItemsSource="{Binding Items}">
...
</CollectionView>
</ScrollView>
<!-- Good: CollectionView sized by Grid (virtualized) -->
<Grid RowDefinitions="Auto,*">
<Label Grid.Row="0"
Text="Messages"/>
<CollectionView Grid.Row="1"
ItemsSource="{Binding Items}"
CachingStrategy="RecycleElement"
RemainingItemsThreshold="5"
RemainingItemsThresholdReachedCommand="{Binding LoadMoreCommand}">
<!-- ItemTemplate -->
</CollectionView>
</Grid>
3. Blocking the UI thread (Usually by accident)
Few developers intentionally block the UI thread. It usually happens indirectly:
- Synchronous file reads
- Waiting on async calls using
.Resultor.Wait() - Heavy JSON parsing during page load
The symptom is familiar: the app doesn’t crash, but taps feel delayed, and animations drop frames.
A good mental model helps here: “Anything that takes noticeable time shouldn’t run on the UI thread.”
Async APIs exist for a reason, and offloading CPU-heavy work prevents the app from feeling “stuck,” even when it’s busy.
Examples:
C#
// I/O-bound - non-blocking
public async Task LoadDataAsync()
{
var json = await File.ReadAllTextAsync(path);
var model = JsonSerializer.Deserialize<MyModel>(json);
MainThread.BeginInvokeOnMainThread(() => MyLabel.Text = model.Name);
}
// CPU-bound - offload work
await Task.Run(() => HeavyComputation());
4. Images that are bigger than your screen (and your memory budget)
High‑resolution images look great until they’re decoded at full size just to appear as tiny thumbnails.
Loading large images directly into the UI increases memory pressure and decoding time, especially on mobile devices. Without caching or downsampling, the same images may be processed repeatedly as users scroll.
In real apps, this often shows up as:
- Sudden memory spikes
- Janky scrolling on image-heavy screens
- Occasional crashes on lower-end devices
The fix isn’t fancy.
- Use images sized for their display
- Cache aggressively
- Downsample early
- Treat images as one of the easiest ways to accidentally hurt performance
Example:
XAML
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:ff="clr-namespace:FFImageLoading.Maui;assembly=FFImageLoading.Maui"
x:Class="YourApp.MainPage">
<StackLayout>
<!--
DownsampleWidth: Decodes the image to a smaller resolution in RAM.
CacheDuration: Keeps the image on disk for the specified number of days.
-->
<ff:CachedImage
Source="https://example.com/huge-photo.jpg"
DownsampleWidth="200"
DownsampleToViewSize="True"
CacheDuration="30"
RetryCount="3"
LoadingPlaceholder="resource_loading.png"
ErrorPlaceholder="resource_error.png"
WidthRequest="200"
HeightRequest="200" />
</StackLayout>
</ContentPage>
5. Reflection-based bindings everywhere
String-based bindings are convenient, but they come at a cost.
At runtime, MAUI relies on reflection to resolve those bindings. That means more rendering overhead and fewer safety nets if something changes in your view model.
Compiled bindings (x:DataType) don’t just improve performance; they improve confidence. You catch mistakes at build time, and the UI does less work when it renders.
In large views or lists, that difference becomes noticeable faster than most people expect.
Example:
XAML
<!-- Reflection (runtime) binding: no x:DataType -->
<DataTemplate>
<Label Text="{Binding Name}" />
</DataTemplate>
<!—Compiled binding-->
<DataTemplate x:DataType="vm:ItemViewModel">
<Label Text="{Binding Name}" />
</DataTemplate>
Read the full blog post on the Syncfusion Website
Top comments (0)