Events
Mar 17, 9 PM - Mar 21, 10 AM
Join the meetup series to build scalable AI solutions based on real-world use cases with fellow developers and experts.
Register nowThis browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
Poor app performance presents itself in many ways. It can make an app seem unresponsive, can cause slow scrolling, and can reduce device battery life. However, optimizing performance involves more than just implementing efficient code. The user's experience of app performance must also be considered. For example, ensuring that operations execute without blocking the user from performing other activities can help to improve the user's experience.
There are many techniques for increasing the performance, and perceived performance, of .NET Multi-platform App UI (.NET MAUI) apps. Collectively these techniques can greatly reduce the amount of work being performed by a CPU, and the amount of memory consumed by an app.
When developing an app, it's important to only attempt to optimize code once it has been profiled. Profiling is a technique for determining where code optimizations will have the greatest effect in reducing performance problems. The profiler tracks the app's memory usage, and records the running time of methods in the app. This data helps to navigate through the execution paths of the app, and the execution cost of the code, so that the best opportunities for optimization can be discovered.
.NET MAUI apps can be profiled using dotnet-trace
on Android, iOS, and Mac, and Windows, and with PerfView on Windows. For more information, see Profiling .NET MAUI apps.
The following best practices are recommended when profiling an app:
Compiled bindings improve data binding performance in .NET MAUI apps by resolving binding expressions at compile time, rather than at runtime with reflection. Compiling a binding expression generates compiled code that typically resolves a binding 8-20 times quicker than using a classic binding. For more information, see Compiled bindings.
Don't use bindings for content that can easily be set statically. There is no advantage in binding data that doesn't need to be bound, because bindings aren't cost efficient. For example, setting Button.Text = "Accept"
has less overhead than binding Button.Text to a viewmodel string
property with value "Accept".
A layout that's capable of displaying multiple children, but that only has a single child, is wasteful. For example, the following example shows a VerticalStackLayout with a single child:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyMauiApp.MainPage">
<VerticalStackLayout>
<Image Source="waterfront.jpg" />
</VerticalStackLayout>
</ContentPage>
This is wasteful and the VerticalStackLayout element should be removed, as shown in the following example:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyMauiApp.MainPage">
<Image Source="waterfront.jpg" />
</ContentPage>
In addition, don't attempt to reproduce the appearance of a specific layout by using combinations of other layouts, as this results in unnecessary layout calculations being performed. For example, don't attempt to reproduce a Grid layout by using a combination of HorizontalStackLayout elements. The following example shows an example of this bad practice:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyMauiApp.MainPage">
<VerticalStackLayout>
<HorizontalStackLayout>
<Label Text="Name:" />
<Entry Placeholder="Enter your name" />
</HorizontalStackLayout>
<HorizontalStackLayout>
<Label Text="Age:" />
<Entry Placeholder="Enter your age" />
</HorizontalStackLayout>
<HorizontalStackLayout>
<Label Text="Occupation:" />
<Entry Placeholder="Enter your occupation" />
</HorizontalStackLayout>
<HorizontalStackLayout>
<Label Text="Address:" />
<Entry Placeholder="Enter your address" />
</HorizontalStackLayout>
</VerticalStackLayout>
</ContentPage>
This is wasteful because unnecessary layout calculations are performed. Instead, the desired layout can be better achieved using a Grid, as shown in the following example:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyMauiApp.MainPage">
<Grid ColumnDefinitions="100,*"
RowDefinitions="30,30,30,30">
<Label Text="Name:" />
<Entry Grid.Column="1"
Placeholder="Enter your name" />
<Label Grid.Row="1"
Text="Age:" />
<Entry Grid.Row="1"
Grid.Column="1"
Placeholder="Enter your age" />
<Label Grid.Row="2"
Text="Occupation:" />
<Entry Grid.Row="2"
Grid.Column="1"
Placeholder="Enter your occupation" />
<Label Grid.Row="3"
Text="Address:" />
<Entry Grid.Row="3"
Grid.Column="1"
Placeholder="Enter your address" />
</Grid>
</ContentPage>
Images are some of the most expensive resources that apps use, and are often captured at high resolutions. While this creates vibrant images full of detail, apps that display such images typically require more CPU usage to decode the image and more memory to store the decoded image. It is wasteful to decode a high resolution image in memory when it will be scaled down to a smaller size for display. Instead, reduce the CPU usage and memory footprint by creating versions of stored images that are close to the predicted display sizes. For example, an image displayed in a list view should most likely be a lower resolution than an image displayed at full-screen.
In addition, images should only be created when required and should be released as soon as the app no longer requires them. For example, if an app is displaying an image by reading its data from a stream, ensure that stream is created only when it's required, and ensure that the stream is released when it's no longer required. This can be achieved by creating the stream when the page is created, or when the Page.Appearing event fires, and then disposing of the stream when the Page.Disappearing event fires.
When downloading an image for display with the ImageSource.FromUri(Uri) method, ensure the downloaded image is cached for a suitable amount of time. For more information, see Image caching.
Reducing the number of elements on a page will make the page render faster. There are two main techniques for achieving this. The first is to hide elements that aren't visible. The IsVisible property of each element determines whether the element should be visible on screen. If an element isn't visible because it's hidden behind other elements, either remove the element or set its IsVisible
property to false
. Setting the IsVisible
property on an element to false
retains the element in the visual tree, but excludes it from rendering and layout calculations.
The second technique is to remove unnecessary elements. For example, the following shows a page layout containing multiple Label elements:
<VerticalStackLayout>
<VerticalStackLayout Padding="20,20,0,0">
<Label Text="Hello" />
</VerticalStackLayout>
<VerticalStackLayout Padding="20,20,0,0">
<Label Text="Welcome to the App!" />
</VerticalStackLayout>
<VerticalStackLayout Padding="20,20,0,0">
<Label Text="Downloading Data..." />
</VerticalStackLayout>
</VerticalStackLayout>
The same page layout can be maintained with a reduced element count, as shown in the following example:
<VerticalStackLayout Padding="20,35,20,20"
Spacing="25">
<Label Text="Hello" />
<Label Text="Welcome to the App!" />
<Label Text="Downloading Data..." />
</VerticalStackLayout>
Any resources that are used throughout the app should be stored in the app's resource dictionary to avoid duplication. This will help to reduce the amount of XAML that has to be parsed throughout the app. The following example shows the HeadingLabelStyle
resource, which is used app wide, and so is defined in the app's resource dictionary:
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyMauiApp.App">
<Application.Resources>
<Style x:Key="HeadingLabelStyle"
TargetType="Label">
<Setter Property="HorizontalOptions"
Value="Center" />
<Setter Property="FontSize"
Value="Large" />
<Setter Property="TextColor"
Value="Red" />
</Style>
</Application.Resources>
</Application>
However, XAML that's specific to a page shouldn't be included in the app's resource dictionary, as the resources will then be parsed at app startup instead of when required by a page. If a resource is used by a page that's not the startup page, it should be placed in the resource dictionary for that page, therefore helping to reduce the XAML that's parsed when the app starts. The following example shows the HeadingLabelStyle
resource, which is only on a single page, and so is defined in the page's resource dictionary:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyMauiApp.MainPage">
<ContentPage.Resources>
<Style x:Key="HeadingLabelStyle"
TargetType="Label">
<Setter Property="HorizontalOptions"
Value="Center" />
<Setter Property="FontSize"
Value="Large" />
<Setter Property="TextColor"
Value="Red" />
</Style>
</ContentPage.Resources>
...
</ContentPage>
For more information about app resources, see Style apps using XAML.
When .NET MAUI builds your app, a linker called ILLink can be used to reduce the overall size of the app. ILLink reduces the size by analyzing the intermediate code produced by the compiler. It removes unused methods, properties, fields, events, structs, and classes to produce an app that contains only code and assembly dependencies that are necessary to run the app.
For more information about configuring the linker behavior, see Linking an Android app, Linking an iOS app, and Linking a Mac Catalyst app.
All apps have an activation period, which is the time between when the app is started and when the app is ready to use. This activation period provides users with their first impression of the app, and so it's important to reduce the activation period and the user's perception of it, in order for them to gain a favorable first impression of the app.
Before an app displays its initial UI, it should provide a splash screen to indicate to the user that the app is starting. If the app can't quickly display its initial UI, the splash screen should be used to inform the user of progress through the activation period, to offer reassurance that the app hasn't hung. This reassurance could be a progress bar, or similar control.
During the activation period, apps execute activation logic, which often includes the loading and processing of resources. The activation period can be reduced by ensuring that required resources are packaged within the app, instead of being retrieved remotely. For example, in some circumstances it may be appropriate during the activation period to load locally stored placeholder data. Then, once the initial UI is displayed, and the user is able to interact with the app, the placeholder data can be progressively replaced from a remote source. In addition, the app's activation logic should only perform work that's required to let the user start using the app. This can help if it delays loading additional assemblies, as assemblies are loaded the first time they are used.
Dependency injection containers introduce additional performance constraints into mobile apps. Registering and resolving types with a container has a performance cost because of the container's use of reflection for creating each type, especially if dependencies are being reconstructed for each page navigation in the app. If there are many or deep dependencies, the cost of creation can increase significantly. In addition, type registration, which usually occurs during app startup, can have a noticeable impact on startup time, dependent upon the container being used. For more information about dependency injection in .NET MAUI apps, see Dependency injection.
As an alternative, dependency injection can be made more performant by implementing it manually using factories.
.NET MAUI Shell apps provide an opinionated navigation experience based on flyouts and tabs. If your app user experience can be implemented with Shell, it is beneficial to do so. Shell apps help to avoid a poor startup experience, because pages are created on demand in response to navigation rather than at app startup, which occurs with apps that use a TabbedPage. For more information, see Shell overview.
When using ListView, there are a number of user experiences that should be optimized:
The ListView control requires an app to supply data and cell templates. How this is achieved will have a large impact on the performance of the control. For more information, see Cache data.
The overall responsiveness of your app can be enhanced, and performance bottlenecks often avoided, by using asynchronous programming. In .NET, the Task-based Asynchronous Pattern (TAP) is the recommended design pattern for asynchronous operations. However, incorrect use of the TAP can result in unperformant apps.
The following general guidelines should be followed when using the TAP:
TaskStatus
enumeration. For more information, see The meaning of TaskStatus and Task status.Task.WhenAll
method to asynchronously wait for multiple asynchronous operations to finish, rather than individually await
a series of asynchronous operations. For more information, see Task.WhenAll.Task.WhenAny
method to asynchronously wait for one of multiple asynchronous operations to finish. For more information, see Task.WhenAny.Task.Delay
method to produce a Task
object that finishes after the specified time. This is useful for scenarios such as polling for data, and delaying handling user input for a predetermined time. For more information, see Task.Delay.Task.Run
method. This method is a shortcut for the TaskFactory.StartNew
method, with the most optimal arguments set. For more information, see Task.Run.await
any initialization. For more information, see Async Constructors on blog.stephencleary.com.TaskCompletionSource<T>
objects. These objects gain the benefits of Task
programmability, and enable you to control the lifetime and completion of the associated Task
. For more information, see The Nature of TaskCompletionSource.Task
object, instead of returning an awaited Task
object, when there's no need to process the result of an asynchronous operation. This is more performant due to less context switching being performed.The following guidelines should be followed when using the TAP with UI controls:
Call an asynchronous version of an API, if it's available. This will keep the UI thread unblocked, which will help to improve the user's experience with the app.
Update UI elements with data from asynchronous operations on the UI thread, to avoid exceptions being thrown. However, updates to the ListView.ItemsSource
property will automatically be marshaled to the UI thread. For information about determining if code is running on the UI thread, see Create a thread on the UI thread.
Important
Any control properties that are updated via data binding will be automatically marshaled to the UI thread.
The following error handling guidelines should be followed when using the TAP:
async void
methods, and instead create async Task
methods. These enable easier error-handling, composability, and testability. The exception to this guideline is asynchronous event handlers, which must return void
. For more information, see Avoid Async Void.Task.Wait
, Task.Result
, or GetAwaiter().GetResult
methods, as they can result in deadlock occurring. However, if this guideline must be violated, the preferred approach is to call the GetAwaiter().GetResult
method because it preserves the task exceptions. For more information, see Async All the Way and Task Exception Handling in .NET 4.5.ConfigureAwait
method whenever possible, to create context-free code. Context-free code has better performance for mobile apps and is a useful technique for avoiding deadlock when working with a partially asynchronous codebase. For more information, see Configure Context.Lazy initialization can be used to defer the creation of an object until it's first used. This technique is primarily used to improve performance, avoid computation, and reduce memory requirements.
Consider using lazy initialization for objects that are expensive to create in the following scenarios:
The Lazy<T>
class is used to define a lazy-initialized type, as shown in the following example:
void ProcessData(bool dataRequired = false)
{
Lazy<double> data = new Lazy<double>(() =>
{
return ParallelEnumerable.Range(0, 1000)
.Select(d => Compute(d))
.Aggregate((x, y) => x + y);
});
if (dataRequired)
{
if (data.Value > 90)
{
...
}
}
}
double Compute(double x)
{
...
}
Lazy initialization occurs the first time the Lazy<T>.Value
property is accessed. The wrapped type is created and returned on first access, and stored for any future access.
For more information about lazy initialization, see Lazy Initialization.
The IDisposable
interface provides a mechanism for releasing resources. It provides a Dispose
method that should be implemented to explicitly release resources. IDisposable
is not a destructor, and should only be implemented in the following circumstances:
IDisposable
resources.Type consumers can then call the IDisposable.Dispose
implementation to free resources when the instance is no longer required. There are two approaches for achieving this:
IDisposable
object in a using
statement.IDisposable.Dispose
in a try
/finally
block.The following example shows how to wrap an IDisposable
object in a using
statement:
public void ReadText(string filename)
{
string text;
using (StreamReader reader = new StreamReader(filename))
{
text = reader.ReadToEnd();
}
...
}
The StreamReader
class implements IDisposable
, and the using
statement provides a convenient syntax that calls the StreamReader.Dispose
method on the StreamReader
object prior to it going out of scope. Within the using
block, the StreamReader
object is read-only and cannot be reassigned. The using
statement also ensures that the Dispose
method is called even if an exception occurs, as the compiler implements the intermediate language (IL) for a try
/finally
block.
The following example shows how to wrap the call to IDisposable.Dispose
in a try
/finally
block:
public void ReadText(string filename)
{
string text;
StreamReader reader = null;
try
{
reader = new StreamReader(filename);
text = reader.ReadToEnd();
}
finally
{
if (reader != null)
reader.Dispose();
}
...
}
The StreamReader
class implements IDisposable
, and the finally
block calls the StreamReader.Dispose
method to release the resource. For more information, see IDisposable Interface.
To prevent memory leaks, events should be unsubscribed from before the subscriber object is disposed of. Until the event is unsubscribed from, the delegate for the event in the publishing object has a reference to the delegate that encapsulates the subscriber's event handler. As long as the publishing object holds this reference, garbage collection will not reclaim the subscriber object memory.
The following example shows how to unsubscribe from an event:
public class Publisher
{
public event EventHandler MyEvent;
public void OnMyEventFires()
{
if (MyEvent != null)
MyEvent(this, EventArgs.Empty);
}
}
public class Subscriber : IDisposable
{
readonly Publisher _publisher;
public Subscriber(Publisher publish)
{
_publisher = publish;
_publisher.MyEvent += OnMyEventFires;
}
void OnMyEventFires(object sender, EventArgs e)
{
Debug.WriteLine("The publisher notified the subscriber of an event");
}
public void Dispose()
{
_publisher.MyEvent -= OnMyEventFires;
}
}
The Subscriber
class unsubscribes from the event in its Dispose
method.
Reference cycles can also occur when using event handlers and lambda syntax, as lambda expressions can reference and keep objects alive. Therefore, a reference to the anonymous method can be stored in a field and used to unsubscribe from the event, as shown in the following example:
public class Subscriber : IDisposable
{
readonly Publisher _publisher;
EventHandler _handler;
public Subscriber(Publisher publish)
{
_publisher = publish;
_handler = (sender, e) =>
{
Debug.WriteLine("The publisher notified the subscriber of an event");
};
_publisher.MyEvent += _handler;
}
public void Dispose()
{
_publisher.MyEvent -= _handler;
}
}
The _handler
field maintains the reference to the anonymous method, and is used for event subscription and unsubscribe.
In some situations it's possible to create strong reference cycles that could prevent objects from having their memory reclaimed by the garbage collector. For example, consider the case where an NSObject-derived subclass, such as a class that inherits from UIView, is added to an NSObject
-derived container and is strongly referenced from Objective-C, as shown in the following example:
class Container : UIView
{
public void Poke()
{
// Call this method to poke this object
}
}
class MyView : UIView
{
Container _parent;
public MyView(Container parent)
{
_parent = parent;
}
void PokeParent()
{
_parent.Poke();
}
}
var container = new Container();
container.AddSubview(new MyView(container));
When this code creates the Container
instance, the C# object will have a strong reference to an Objective-C object. Similarly, the MyView
instance will also have a strong reference to an Objective-C object.
In addition, the call to container.AddSubview
will increase the reference count on the unmanaged MyView
instance. When this happens, the .NET for iOS runtime creates a GCHandle
instance to keep the MyView
object in managed code alive, because there is no guarantee that any managed objects will keep a reference to it. From a managed code perspective, the MyView
object would be reclaimed after the AddSubview(UIView) call were it not for the GCHandle
.
The unmanaged MyView
object will have a GCHandle
pointing to the managed object, known as a strong link. The managed object will contain a reference to the Container
instance. In turn the Container
instance will have a managed reference to the MyView
object.
In circumstances where a contained object keeps a link to its container, there are several options available to deal with the circular reference:
Dispose
on the objects.null
.One way to prevent a cycle is to use a weak reference from the child to the parent. For example, the above code could be as shown in the following example:
class Container : UIView
{
public void Poke()
{
// Call this method to poke this object
}
}
class MyView : UIView
{
WeakReference<Container> _weakParent;
public MyView(Container parent)
{
_weakParent = new WeakReference<Container>(parent);
}
void PokeParent()
{
if (weakParent.TryGetTarget (out var parent))
parent.Poke();
}
}
var container = new Container();
container.AddSubview(new MyView container));
Here, the contained object will not keep the parent alive. However, the parent keeps the child alive through the call to container.AddSubView
.
This also happens in iOS APIs that use the delegate or data source pattern, where a peer class contains the implementation. For example, when setting the Delegate property or the DataSource in the UITableView class.
In the case of classes that are created purely for the sake of implementing a protocol, for example the IUITableViewDataSource, what you can do is instead of creating a subclass, you can just implement the interface in the class and override the method, and assign the DataSource
property to this
.
If a strong reference exists and it's difficult to remove the dependency, make a Dispose
method clear the parent pointer.
For containers, override the Dispose
method to remove the contained objects, as shown in the following example:
class MyContainer : UIView
{
public override void Dispose()
{
// Brute force, remove everything
foreach (var view in Subviews)
{
view.RemoveFromSuperview();
}
base.Dispose();
}
}
For a child object that keeps strong reference to its parent, clear the reference to the parent in the Dispose
implementation:
class MyChild : UIView
{
MyContainer _container;
public MyChild(MyContainer container)
{
_container = container;
}
public override void Dispose()
{
_container = null;
}
}
.NET MAUI feedback
.NET MAUI is an open source project. Select a link to provide feedback:
Events
Mar 17, 9 PM - Mar 21, 10 AM
Join the meetup series to build scalable AI solutions based on real-world use cases with fellow developers and experts.
Register nowTraining
Learning path
Use advance techniques in canvas apps to perform custom updates and optimization - Training
Use advance techniques in canvas apps to perform custom updates and optimization
Documentation
Compiled bindings can be used to improve data binding performance in .NET MAUI applications.
Linking a .NET MAUI Android app - .NET MAUI
Learn about the .NET for Android linker, which is used to eliminate unused code from a .NET MAUI Android app in order to reduce its size.
Trim a .NET MAUI app - .NET MAUI
Learn about the .NET trimmer, which eliminates unused code from a .NET MAUI app to reduce its size.