Úvod: Proč je výkon v mobilních aplikacích klíčový
Uživatelé mobilních aplikací jsou nemilosrdní — a upřímně, kdo by jim to vyčítal? Studie opakovaně ukazují, že pokud se aplikace nespustí do 2–3 sekund, většina lidí ji prostě zavře a nikdy se k ní nevrátí. A to je teprve začátek. Pomalé scrollování, zasekávající se animace nebo nadměrná spotřeba paměti dokážou spolehlivě zničit i aplikaci s perfektním designem a skvělou funkcionalitou.
.NET MAUI jako cross-platform framework přináší obrovské výhody v produktivitě, ale zároveň přidává abstrakční vrstvy, které mohou výkon ovlivnit. Dobrá zpráva? S každou novou verzí .NET tým Microsoftu výrazně zlepšuje výkonnostní charakteristiky MAUI. V .NET 9 a 10 přišly zásadní optimalizace — kompilované bindingy, podpora NativeAOT nebo přepracované handlery pro CollectionView.
Tak pojďme na to. V tomto článku si projdeme kompletní strategie pro optimalizaci výkonu .NET MAUI aplikací, od rychlosti startupu přes vykreslování UI až po správu paměti a profilování. Všechno s praktickými příklady kódu, které můžete rovnou použít ve svých projektech.
Optimalizace startupu aplikace
Startup je první dojem, který vaše aplikace udělá. A v mobilním světě bývá první dojem často i poslední.
Pojďme se podívat na klíčové techniky, jak spuštění vaší MAUI aplikace výrazně zrychlit.
Odlehčení MauiProgram.cs
Soubor MauiProgram.cs je vstupním bodem aplikace a všechno, co se v něm děje, přímo ovlivňuje dobu startupu. Základní pravidlo je jednoduché: registrujte jen to, co skutečně potřebujete při spuštění.
using Microsoft.Extensions.Logging;
namespace MauiPerfApp;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
// Registrujte pouze fonty, které skutečně používáte
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// Singleton služby — vytvořeny jednou při prvním použití
builder.Services.AddSingleton<IApiService, ApiService>();
builder.Services.AddSingleton<IDatabaseService, DatabaseService>();
// Transient služby — nová instance při každém požadavku
builder.Services.AddTransient<MainPage>();
builder.Services.AddTransient<MainViewModel>();
// ŠPATNĚ: Neregistrujte služby, které nejsou potřeba okamžitě
// builder.Services.AddSingleton<IAnalyticsService, HeavyAnalyticsService>();
// SPRÁVNĚ: Použijte lazy inicializaci pro těžké služby
builder.Services.AddSingleton<Lazy<IAnalyticsService>>(sp =>
new Lazy<IAnalyticsService>(() => new HeavyAnalyticsService()));
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}
Lazy Loading služeb a stránek
Ne každá stránka a služba musí být připravená v okamžiku startu. Vzor Lazy<T> vám umožní odložit vytvoření instance až na moment, kdy ji opravdu potřebujete:
public class MainViewModel : ObservableObject
{
private readonly Lazy<IAnalyticsService> _analytics;
private readonly Lazy<IExportService> _export;
public MainViewModel(
Lazy<IAnalyticsService> analytics,
Lazy<IExportService> export)
{
_analytics = analytics;
_export = export;
}
[RelayCommand]
private async Task TrackEventAsync()
{
// Instance se vytvoří až zde, při prvním přístupu
await _analytics.Value.TrackAsync("button_clicked");
}
[RelayCommand]
private async Task ExportDataAsync()
{
// Export service se inicializuje až když uživatel
// skutečně klikne na export
await _export.Value.ExportToCsvAsync(Data);
}
}
Tenhle přístup může výrazně snížit dobu startupu, protože se vytváří jen ty služby, které jsou okamžitě potřeba pro zobrazení první stránky. V jednom z mých projektů jsme takhle ušetřili skoro 400 ms na startu — a to není málo.
Asynchronní inicializace
Pokud vaše aplikace potřebuje při startu načíst data (konfigurace, uživatelský profil, cache), nikdy to nedělejte synchronně. Místo toho zobrazte lehký splash screen a data načtěte na pozadí:
public partial class App : Application
{
public App(IServiceProvider serviceProvider)
{
InitializeComponent();
}
protected override Window CreateWindow(IActivationState? activationState)
{
return new Window(new AppShell());
}
}
// V AppShell nebo na hlavní stránce:
public partial class MainPage : ContentPage
{
private readonly MainViewModel _viewModel;
public MainPage(MainViewModel viewModel)
{
InitializeComponent();
BindingContext = _viewModel = viewModel;
}
protected override async void OnAppearing()
{
base.OnAppearing();
// Načtení dat až po zobrazení stránky
await _viewModel.InitializeAsync();
}
}
Kompilované bindingy: 8× rychlejší data binding
Kompilované bindingy jsou asi nejvýznamnější výkonnostní vylepšení v .NET MAUI. Místo toho, aby se binding výrazy řešily za běhu pomocí reflexe, jsou kompilované bindingy zpracovány už při kompilaci. Výsledek? Až 8× rychlejší výkon u OneWay bindingů a až 20× rychlejší u OneTime bindingů.
To jsou čísla, která stojí za pozornost.
Jak aktivovat kompilované bindingy
Klíčem je atribut x:DataType, který řekne kompilátoru, jaký typ dat se na daném místě binduje:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:MauiPerfApp.ViewModels"
x:Class="MauiPerfApp.Views.ProductListPage"
x:DataType="vm:ProductListViewModel">
<StackLayout Padding="16">
<Label Text="{Binding Title}"
FontSize="24"
FontAttributes="Bold" />
<Label Text="{Binding ProductCount, StringFormat='Celkem {0} produktů'}"
TextColor="Gray" />
<CollectionView ItemsSource="{Binding Products}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="vm:ProductItemViewModel">
<Grid Padding="8" ColumnDefinitions="*,Auto">
<Label Text="{Binding Name}"
FontSize="16"
VerticalOptions="Center" />
<Label Grid.Column="1"
Text="{Binding Price, StringFormat='{0:C}'}"
FontSize="16"
FontAttributes="Bold"
VerticalOptions="Center" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
</ContentPage>
Důležitá pravidla pro kompilované bindingy
Při práci s kompilovanými bindingy je potřeba mít na paměti pár věcí:
- Nastavte
x:DataTypena každé úrovni, kde se mění typ kontextu — typicky na stránce a vDataTemplate. - Potřebujete dynamický binding? V situaci, kdy typ není známý v době kompilace, můžete lokálně vypnout kompilované bindingy nastavením
x:DataType="x:Null". - V .NET 10 jsou kompilované bindingy povinné pro aplikace s NativeAOT nebo full trimming — string-based bindingy v těchto režimech prostě nefungují.
- Chyby v bindingech se chytí už při kompilaci, což je vlastně docela fajn bonus — žádné překvapení za běhu.
<!-- Příklad: Vypnutí kompilovaných bindingů pro specifický případ -->
<ContentView x:DataType="x:Null">
<!-- Zde se použijí klasické runtime bindingy -->
<Label Text="{Binding DynamicProperty}" />
</ContentView>
<!-- Příklad: Opětovné zapnutí pro vnořený prvek -->
<StackLayout x:DataType="vm:DetailViewModel">
<Label Text="{Binding Description}" />
</StackLayout>
Optimalizace UI vykreslování
Vykreslování uživatelského rozhraní je oblast, kde se výkonnostní problémy projevují úplně nejvíc — zasekávání při scrollování, zpožděné animace nebo celkově ten nepříjemný „pomalý" pocit z aplikace. Tohle uživatelé odpouští jen těžko.
Zploštění vizuální hierarchie
Každý vnořený layout vyžaduje měření a vykreslení. Hluboce vnořené layouty výrazně zpomalují rendering. Řešení? Používejte Grid s definovanými řádky a sloupci místo vnořování:
<!-- ŠPATNĚ: Hluboce vnořené layouty -->
<StackLayout>
<HorizontalStackLayout>
<VerticalStackLayout>
<Label Text="{Binding Name}" />
<Label Text="{Binding Email}" />
</VerticalStackLayout>
<VerticalStackLayout>
<Image Source="{Binding Avatar}" />
</VerticalStackLayout>
</HorizontalStackLayout>
</StackLayout>
<!-- SPRÁVNĚ: Plochý Grid -->
<Grid ColumnDefinitions="*,Auto"
RowDefinitions="Auto,Auto"
Padding="8">
<Label Text="{Binding Name}"
FontSize="16"
FontAttributes="Bold" />
<Label Grid.Row="1"
Text="{Binding Email}"
TextColor="Gray" />
<Image Grid.Column="1"
Grid.RowSpan="2"
Source="{Binding Avatar}"
HeightRequest="48"
WidthRequest="48" />
</Grid>
Optimalizace CollectionView
CollectionView je výkonnější než starší ListView díky vestavěné virtualizaci — vykresluje pouze prvky viditelné na obrazovce. Ale i tady existují úskalí, na která je třeba dávat pozor:
<CollectionView ItemsSource="{Binding Items}"
ItemSizingStrategy="MeasureFirstItem">
<!--
MeasureFirstItem: Změří pouze první položku a předpokládá,
že ostatní mají stejnou velikost. Výrazně rychlejší než
výchozí MeasureAllItems.
-->
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:ItemModel">
<!--
DŮLEŽITÉ: Nepoužívejte VerticalStackLayout nebo
HorizontalStackLayout jako kořenový prvek DataTemplate.
Může to způsobit memory leaky při scrollování.
Používejte Grid nebo obalte layout do Border.
-->
<Border Stroke="Transparent" StrokeThickness="0">
<Grid Padding="12"
ColumnDefinitions="48,*,Auto"
ColumnSpacing="12">
<Image Source="{Binding ThumbnailUrl}"
Aspect="AspectFill"
HeightRequest="48"
WidthRequest="48" />
<Label Grid.Column="1"
Text="{Binding Title}"
LineBreakMode="TailTruncation"
VerticalOptions="Center" />
<Label Grid.Column="2"
Text="{Binding Price, StringFormat='{0:C}'}"
VerticalOptions="Center" />
</Grid>
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
Optimalizace obrázků
Obrázky jsou jedním z největších zdrojů výkonnostních problémů. Načítání velkých obrázků žere paměť a zpomaluje rendering. Tohle se vyplatí řešit hned od začátku:
public class ImageOptimizationService
{
// Cache pro dekódované obrázky
private readonly Dictionary<string, ImageSource> _cache = new();
public ImageSource GetOptimizedImage(string url, int maxWidth = 200)
{
var cacheKey = $"{url}_{maxWidth}";
if (_cache.TryGetValue(cacheKey, out var cached))
return cached;
// Použití UriImageSource s cachováním
var source = new UriImageSource
{
Uri = new Uri(url),
CachingEnabled = true,
CacheValidity = TimeSpan.FromDays(7)
};
_cache[cacheKey] = source;
return source;
}
}
// V XAML:
// Vždy specifikujte přesné rozměry obrázků
// <Image Source="{Binding ImageSource}"
// HeightRequest="120"
// WidthRequest="120"
// Aspect="AspectFill" />
Správa paměti a prevence memory leaků
Memory leaky v mobilních aplikacích jsou zákeřné. Aplikace může fungovat zdánlivě správně, ale postupně spotřebovává víc a víc paměti, až ji systém násilně ukončí. Klasika.
V .NET MAUI jsou nejčastější příčiny memory leaků spojené s event handlery a životním cyklem stránek.
Odhlašování z event handlerů
Nejčastější příčinou memory leaků v MAUI aplikacích je zapomenuté odhlášení z eventů. Pokud se stránka přihlásí k odběru události na dlouhodobě žijícím objektu (třeba MessagingCenter nebo vlastní služba), stránka se nikdy neuvolní z paměti. To je problém, na který jsem osobně narazil víckrát, než bych chtěl přiznat:
public partial class DetailPage : ContentPage
{
private readonly INotificationService _notifications;
private readonly EventHandler<NotificationEventArgs> _handler;
public DetailPage(INotificationService notifications)
{
InitializeComponent();
_notifications = notifications;
// Uložíme referenci na handler pro pozdější odhlášení
_handler = OnNotificationReceived;
}
protected override void OnAppearing()
{
base.OnAppearing();
// Přihlášení k odběru při zobrazení stránky
_notifications.NotificationReceived += _handler;
}
protected override void OnDisappearing()
{
base.OnDisappearing();
// KRITICKÉ: Odhlášení při opuštění stránky
_notifications.NotificationReceived -= _handler;
}
private void OnNotificationReceived(object? sender, NotificationEventArgs e)
{
// Zpracování notifikace
}
}
Použití WeakEventManager
Pro scénáře, kde je obtížné zajistit správné odhlášení, nabízí .NET MAUI vzor WeakEventManager. Ten drží pouze slabé reference na posluchače, takže vám garbage collector poděkuje:
public class NotificationService : INotificationService
{
private readonly WeakEventManager _eventManager = new();
public event EventHandler<NotificationEventArgs> NotificationReceived
{
add => _eventManager.AddEventHandler(value);
remove => _eventManager.RemoveEventHandler(value);
}
public void RaiseNotification(string message)
{
_eventManager.HandleEvent(
this,
new NotificationEventArgs(message),
nameof(NotificationReceived));
}
}
Sledování a diagnostika paměti
Pro systematické sledování využití paměti si můžete implementovat jednoduchý diagnostický nástroj. Nic složitého, ale hodně to pomůže při hledání problémů:
public class MemoryDiagnostics
{
private readonly ILogger<MemoryDiagnostics> _logger;
public MemoryDiagnostics(ILogger<MemoryDiagnostics> logger)
{
_logger = logger;
}
public void LogMemoryUsage(string context = "")
{
var gcMemory = GC.GetTotalMemory(forceFullCollection: false);
var gcMemoryMb = gcMemory / (1024.0 * 1024.0);
_logger.LogInformation(
"[Paměť] {Context} - GC paměť: {Memory:F2} MB, " +
"Gen0: {Gen0}, Gen1: {Gen1}, Gen2: {Gen2}",
context,
gcMemoryMb,
GC.CollectionCount(0),
GC.CollectionCount(1),
GC.CollectionCount(2));
}
public void ForceCleanup()
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
LogMemoryUsage("Po vyčištění");
}
}
NativeAOT: Budoucnost výkonu .NET MAUI
NativeAOT (Ahead-of-Time kompilace) je technologie, která zkompiluje .NET kód přímo do nativního strojového kódu ještě před spuštěním aplikace. Na rozdíl od JIT kompilace, která převádí IL kód za běhu, NativeAOT vytvoří plně nativní binárku. A ty výsledky stojí za to.
Výhody NativeAOT
- Až 2× rychlejší startup oproti Mono runtime na iOS
- Až 50% menší velikost aplikace pro template projekty
- Nižší spotřeba paměti — žádný JIT kompilátor v paměti
- Konzistentní výkon — žádné JIT warm-up zpoždění
Jak aktivovat NativeAOT
V .NET 10 je NativeAOT dostupný pro iOS, Mac Catalyst a Windows. Na Androidu zatím bohužel není podporován. Aktivace vyžaduje úpravy v .csproj souboru:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
<OutputType>Exe</OutputType>
<UseMaui>true</UseMaui>
<!-- Aktivace NativeAOT -->
<PublishAot>true</PublishAot>
<!-- Trimming je vyžadován pro NativeAOT -->
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<!-- Volitelné: Potlačení AOT varování během vývoje -->
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
</PropertyGroup>
</Project>
Požadavky na kompatibilitu
NativeAOT přináší určitá omezení, se kterými musíte počítat. Nic nepřekonatelného, ale je dobré o nich vědět předem:
// ŠPATNĚ: Reflexe nefunguje spolehlivě s NativeAOT
var type = Type.GetType("MauiApp.Services.MyService");
var instance = Activator.CreateInstance(type);
// SPRÁVNĚ: Přímé vytváření instancí nebo DI
var instance = new MyService();
// nebo lépe — přes dependency injection
services.AddSingleton<IMyService, MyService>();
// ŠPATNĚ: Dynamické generování kódu
var dynamicMethod = new DynamicMethod("Calculate", typeof(int),
new[] { typeof(int) });
// SPRÁVNĚ: Statický kód nebo source generátory
[GeneratedRegex(@"\d+")]
private static partial Regex NumberPattern();
// DŮLEŽITÉ: Kompilované bindingy jsou POVINNÉ s NativeAOT
// String-based bindingy nebudou fungovat!
Trimming a redukce velikosti aplikace
Trimming odstraňuje nepoužívaný kód z výsledné binárky, což vede k menší aplikaci a rychlejšímu startupu. I bez NativeAOT je trimming cenným nástrojem pro optimalizaci — a jeho nastavení je poměrně jednoduché.
Konfigurace trimmingu
<PropertyGroup>
<!-- Základní trimming -->
<PublishTrimmed>true</PublishTrimmed>
<!-- Režimy trimmingu:
"partial" - konzervativní, odstraní jen explicitně označený kód
"full" - agresivní, odstraní vše nepoužívané
-->
<TrimMode>full</TrimMode>
<!-- Pro ladění: zobrazí varování o potenciálních problémech -->
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
</PropertyGroup>
Označení kódu jako kompatibilního s trimmingem
Pokud váš kód používá vzory, které trimmer nedokáže analyzovat staticky (reflexe, dynamické načítání), musíte ho odpovídajícím způsobem anotovat:
using System.Diagnostics.CodeAnalysis;
public class PluginLoader
{
// Atribut informuje trimmer, že metoda vyžaduje
// zachování veřejných konstruktorů typu T
[RequiresUnreferencedCode("Používá reflexi pro načítání pluginů")]
public T LoadPlugin<T>() where T : class
{
// Reflexe-based kód
return Activator.CreateInstance<T>();
}
// Alternativa kompatibilní s trimmingem
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors,
typeof(MyPlugin))]
public IPlugin LoadMyPlugin()
{
return new MyPlugin();
}
}
Profilování a měření výkonu
Bez měření je optimalizace jen hádání. Tohle nemůžu zdůraznit dost. .NET MAUI nabízí několik nástrojů pro přesné profilování výkonu, tak je pojďme využít.
Diagnostický middleware
Implementujte si vlastní měření kritických operací pomocí Stopwatch. Je to jednoduchý, ale překvapivě účinný způsob, jak najít úzká hrdla:
using System.Diagnostics;
public class PerformanceTracker
{
private readonly ILogger<PerformanceTracker> _logger;
private readonly Dictionary<string, List<long>> _measurements = new();
public PerformanceTracker(ILogger<PerformanceTracker> logger)
{
_logger = logger;
}
public IDisposable Track(string operationName)
{
return new TrackingScope(this, operationName);
}
private void RecordMeasurement(string name, long elapsedMs)
{
if (!_measurements.ContainsKey(name))
_measurements[name] = new List<long>();
_measurements[name].Add(elapsedMs);
_logger.LogInformation(
"[Výkon] {Operation}: {Elapsed}ms (průměr: {Avg:F1}ms)",
name, elapsedMs, _measurements[name].Average());
}
public string GetReport()
{
var sb = new System.Text.StringBuilder();
sb.AppendLine("=== Výkonnostní report ===");
foreach (var (name, times) in _measurements)
{
sb.AppendLine($"{name}:");
sb.AppendLine($" Počet: {times.Count}");
sb.AppendLine($" Průměr: {times.Average():F1}ms");
sb.AppendLine($" Min: {times.Min()}ms");
sb.AppendLine($" Max: {times.Max()}ms");
}
return sb.ToString();
}
private class TrackingScope : IDisposable
{
private readonly PerformanceTracker _tracker;
private readonly string _name;
private readonly Stopwatch _sw;
public TrackingScope(PerformanceTracker tracker, string name)
{
_tracker = tracker;
_name = name;
_sw = Stopwatch.StartNew();
}
public void Dispose()
{
_sw.Stop();
_tracker.RecordMeasurement(_name, _sw.ElapsedMilliseconds);
}
}
}
// Použití:
public class ProductRepository
{
private readonly PerformanceTracker _perf;
public async Task<List<Product>> GetAllAsync()
{
using (_perf.Track("ProductRepository.GetAllAsync"))
{
// Databázový dotaz
return await _db.Table<Product>().ToListAsync();
}
}
}
Profilování s dotnet-trace a dotnet-gcdump
Pro detailní profilování v produkčním prostředí můžete použít nástroje z .NET CLI. Nástroj dotnet-gcdump vytvoří snapshot spravované paměti, který pak můžete analyzovat:
# Instalace nástrojů
dotnet tool install -g dotnet-trace
dotnet tool install -g dotnet-gcdump
# Zachycení trace (vyžaduje připojení k běžící aplikaci)
dotnet-trace collect --process-id <PID> --duration 00:00:30
# Vytvoření GC dumpu pro analýzu paměti
dotnet-gcdump collect --process-id <PID>
# Analýza dumpu
dotnet-gcdump report <dump-file>
Pro vizuální analýzu doporučuji rozšíření .NET Meteor pro VS Code nebo nástroj Speedscope, který zobrazí data z trace souboru jako interaktivní flame graph. Flame graphy jsou mimochodem skvělý způsob, jak rychle identifikovat, co vám žere nejvíc času.
Měření v Release buildu
Tady je jedna věc, kterou je opravdu důležité zmínit: vždy profilujte Release build. Debug build používá interpreter pro podporu C# Hot Reload, což výrazně zkresluje výsledky. Porovnávání výkonu na Debug buildu je jako měření rychlosti auta s ruční brzdou:
# Build pro profilování
dotnet build -c Release
# Nebo publikování s kompletní optimalizací
dotnet publish -c Release -f net10.0-android
Optimalizace síťové komunikace
Síťové požadavky jsou dalším častým zdrojem výkonnostních problémů. Pomalé API volání blokují UI a kazí uživatelský zážitek — a v mobilním světě, kde je připojení často nestabilní, to platí dvojnásob.
Implementace cachování HTTP odpovědí
public class CachedApiService : IApiService
{
private readonly HttpClient _httpClient;
private readonly IMemoryCache _cache;
private readonly IConnectivity _connectivity;
public CachedApiService(
HttpClient httpClient,
IMemoryCache cache,
IConnectivity connectivity)
{
_httpClient = httpClient;
_cache = cache;
_connectivity = connectivity;
}
public async Task<List<Product>> GetProductsAsync()
{
const string cacheKey = "products_list";
// Zkusit cache
if (_cache.TryGetValue(cacheKey, out List<Product>? cached))
return cached!;
// Ověřit konektivitu
if (_connectivity.NetworkAccess != NetworkAccess.Internet)
return new List<Product>();
// Načíst ze serveru
var response = await _httpClient.GetAsync("api/products");
response.EnsureSuccessStatusCode();
var products = await response.Content
.ReadFromJsonAsync<List<Product>>();
// Uložit do cache s expirací
var cacheOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromMinutes(5))
.SetAbsoluteExpiration(TimeSpan.FromMinutes(30));
_cache.Set(cacheKey, products, cacheOptions);
return products ?? new List<Product>();
}
}
Efektivní stránkování a inkrementální načítání
Pro velké datové sady nikdy nenačítejte všechno najednou. Implementujte inkrementální načítání, které spolupracuje s virtualizací CollectionView:
public partial class ProductListViewModel : ObservableObject
{
private readonly IApiService _api;
private int _currentPage = 0;
private const int PageSize = 20;
[ObservableProperty]
private ObservableCollection<Product> _products = new();
[ObservableProperty]
private bool _isLoadingMore;
[RelayCommand]
private async Task LoadMoreAsync()
{
if (IsLoadingMore) return;
IsLoadingMore = true;
try
{
var newItems = await _api.GetProductsAsync(
page: _currentPage,
pageSize: PageSize);
foreach (var item in newItems)
{
Products.Add(item);
}
_currentPage++;
}
finally
{
IsLoadingMore = false;
}
}
}
<!-- V XAML: RemainingItemsThreshold spustí načítání
dalších dat před dosažením konce seznamu -->
<CollectionView ItemsSource="{Binding Products}"
RemainingItemsThreshold="5"
RemainingItemsThresholdReachedCommand="{Binding LoadMoreCommand}">
<CollectionView.Footer>
<ActivityIndicator IsRunning="{Binding IsLoadingMore}"
IsVisible="{Binding IsLoadingMore}"
HorizontalOptions="Center"
Margin="0,16" />
</CollectionView.Footer>
</CollectionView>
Kontrolní seznam pro výkonnostní audit
Na závěr přidávám přehledný kontrolní seznam, který můžete použít jako referenci při optimalizaci vaší .NET MAUI aplikace. Doporučuji si ho projít před každým release.
Startup
- Registrujte v DI kontejneru pouze nezbytné služby
- Použijte
Lazy<T>pro těžké služby, které nejsou potřeba při startu - Načítejte data asynchronně po zobrazení první stránky
- Minimalizujte počet registrovaných fontů
Data binding
- Používejte
x:DataTypepro kompilované bindingy na všech stránkách - Nastavte
x:DataTypev každémDataTemplate - Preferujte
OneTimebinding mode tam, kde není potřeba aktualizace
UI a rendering
- Používejte
Gridmísto vnořenýchStackLayout - Nastavte
ItemSizingStrategy="MeasureFirstItem"naCollectionView - Obalte obsah
DataTemplatedoBorderpro prevenci memory leaků - Vždy specifikujte rozměry obrázků (
HeightRequest,WidthRequest) - Aktivujte cachování pro
UriImageSource
Paměť
- Odhlaste se z event handlerů v
OnDisappearing - Používejte
WeakEventManagerpro dlouhodobě žijící služby - Pravidelně profilujte paměť a sledujte trendy
Build a deployment
- Aktivujte trimming pro produkční buildy
- Zvažte NativeAOT pro iOS a Mac Catalyst
- Vždy profilujte Release build, nikdy Debug
- Monitorujte velikost výsledné binárky
Závěr
Optimalizace výkonu .NET MAUI aplikace není jednorázová aktivita, ale kontinuální proces. Klíč je měřit, identifikovat úzká hrdla a cíleně je řešit. Začněte s nejviditelnějšími problémy — startup a UI rendering — a postupně se posouvejte k hlubším optimalizacím jako NativeAOT a trimming.
Kompilované bindingy s x:DataType jsou pravděpodobně nejjednodušší vylepšení s největším dopadem. Implementace je přímočará a přínos okamžitě viditelný. NativeAOT pak představuje budoucnost výkonu v .NET ekosystému, i když v současné době vyžaduje dodržování určitých pravidel (hlavně žádná reflexe).
Pamatujte: nejlepší optimalizace je ta, která řeší skutečný problém potvrzený měřením. Neoptimalizujte naslepo — profilujte, identifikujte a teprve pak optimalizujte. A hlavně — vždy testujte na reálných zařízeních, protože simulátor vám o výkonu neřekne celou pravdu.