DEV Community

Cover image for 8 .NET MAUI Performance Anti-Patterns That Make Apps Feel Slow
Lucy Muturi for Syncfusion, Inc.

Posted on • Originally published at syncfusion.com on

8 .NET MAUI Performance Anti-Patterns That Make Apps Feel Slow

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 .Result or .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());
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Read the full blog post on the Syncfusion Website

Top comments (0)