Optimalizácia výkonu .NET MAUI 10: štartovací čas, memory leaks a Native AOT
Praktický sprievodca optimalizáciou .NET MAUI 10: Native AOT, XAML compilation, lov memory leakov, profilovanie pomocou dotnet-trace a checklist pred releasom. S reálnymi metrikami pre iOS aj Android.
Optimalizácia výkonu .NET MAUI 10 aplikácie stojí na troch oblastiach: skrátení štartovacieho času pomocou Native AOT a XAML compilation, eliminácii memory leaks cez správne odhlasovanie eventov a profilovaní pomocou dotnet-trace a Visual Studio Profileru. V mojich projektoch sa mi pravidelne darí znížiť cold start na Androide pod 1,2 sekundy a na iOS pod 0,9 sekundy práve kombináciou týchto techník. Tento sprievodca prejde každý krok s reálnymi metrikami a kódom, ktorý môžete nasadiť dnes.
Native AOT v .NET MAUI 10 skracuje cold start na iOS až o 40 % oproti Monu a redukuje binárku o 20 – 30 %.
Najčastejším zdrojom memory leaks v MAUI sú neodhlásené event handlery a statické referencie na Page alebo BindingContext.
XAML compilation (XamlCompilation(XamlCompilationOptions.Compile)) musí byť zapnutá globálne. Bez nej sa parsuje XAML za behu a štart sa predlžuje o stovky milisekúnd.
Trimming s TrimMode=full a PublishTrimmed=true šetrí 30 – 50 MB veľkosti APK, vyžaduje však anotácie reflexie.
CollectionView musí používať RemainingItemsThreshold a IsGrouped=false všade tam, kde to ide. DataTemplate selektory bývajú top hot path.
Pre profilovanie použite dotnet-trace na zachytenie startup traces a Visual Studio Diagnostic Tools na heap snapshoty.
Prečo je moja .NET MAUI aplikácia pomalá?
V praxi existujú štyri opakujúce sa príčiny: pomalý cold start kvôli Mono interpreteru a nekompilovanému XAML, neukončené event handlery držiace pri živote celé stránky, neefektívne renderovanie CollectionView a chýbajúci trimming s linkerom. .NET MAUI 10 priniesol v máji 2026 oficiálnu podporu Native AOT pre iOS a Mac Catalyst, čo zásadne mení obraz výkonu na Apple platformách. Pre Android stále platí, že kombinácia UseInterpreter=false, R8 shrinkeru a XAML compilation dáva najlepšie výsledky.
Pri audite pomalých MAUI aplikácií si všímam jeden opakujúci sa vzor: vývojári merajú výkon iba v Debug konfigurácii. Debug build používa interpreter, vypína trimming, nepoužíva ngen ani Native AOT a obsahuje diagnostické symboly. Reálna metrika sa dá získať iba na Release configu s rovnakými flagmi, aké pôjdu do obchodu. Druhý častý omyl? Meranie na emulátore. Emulátor na ARM Macu má často lepší štart než reálne Android zariadenie strednej triedy, takže výsledky klamú smerom k optimizmu.
Ako merať štartovací čas .NET MAUI aplikácie
Predtým, než začnete čokoľvek optimalizovať, potrebujete baseline. Pre Android použite adb shell am start -W, ktorý vracia TotalTime aj WaitTime. Pre iOS použite Instruments → App Launch template. Oba meriam aspoň 10× za sebou, zahodím prvé dva behy (warm caches) a beriem medián zvyšku. Priemer je príliš citlivý na outliers.
# Android cold start meranie
adb shell am force-stop com.mojafirma.mojaapp
adb shell am start -W -n com.mojafirma.mojaapp/crc64xxxxxxxxxxxx.MainActivity
# Výsledok obsahuje:
# Status: ok
# Activity: com.mojafirma.mojaapp/crc64xxxxxxxxxxxx.MainActivity
# TotalTime: 1142
# WaitTime: 1189
V kóde samotnej aplikácie pridajte Stopwatch do MauiProgram.CreateMauiApp a do App konštruktora, aby ste vedeli, koľko času sa stratí v DI registráciách a koľko v inicializácii stránky. V mojej referenčnej MAUI 10 aplikácii (50 strán, 30 view modelov) trvá CreateMauiApp okolo 180 ms, prvé AppShell renderovanie 240 ms a prvý OnAppearing ďalších 90 ms.
Native AOT v .NET MAUI 10: kedy a ako
Native AOT v .NET MAUI 10 generuje natívny binárny kód už pri publishi, takže pri spustení nie je potrebný JIT ani Mono interpreter. Oficiálne je podporovaný na iOS, Mac Catalyst a Mac od MAUI 10 RTM. Na Androide je v preview stave a vyžaduje net10.0-android s $(RuntimeIdentifier) nastaveným na android-arm64. Detaily a aktuálny stav nájdete v oficiálnom .NET MAUI roadmape na GitHube.
Reálny dopad na výkon v aplikácii, ktorú som migroval minulý mesiac:
Metrika
Mono (default)
Native AOT / Profiled AOT
iOS cold start (iPhone 13)
1 480 ms
890 ms
Android cold start (Pixel 6a)
1 720 ms
1 180 ms
Veľkosť iOS .ipa
78 MB
61 MB
Veľkosť Android .apk (arm64)
52 MB
34 MB
Spotreba RAM po štarte
112 MB
89 MB
Čas buildu (CI)
3 min 40 s
9 min 10 s
Pozor. Native AOT zakazuje run-time reflection a dynamicky generovaný kód. Knižnice používajúce System.Reflection.Emit (napríklad niektoré IoC kontajnery, AutoMapper bez source generátora) prestanú fungovať. Použite CommunityToolkit.Mvvm so source generátormi, ktorý je AOT-friendly, namiesto behaviour riešiacich reflection. Viac o praktickej integrácii MVVM nájdete v našom hĺbkovom rozbore MVVM architektúry v .NET MAUI.
XAML compilation a trimming bez nepríjemných prekvapení
XAML compilation je v MAUI 10 zapnutá štandardne, no stačí jeden assembly s atribútom [XamlCompilation(XamlCompilationOptions.Skip)] a parser sa spúšťa za behu pre celé sub-stromy. Overte si to globálnym atribútom v AssemblyInfo.cs:
using Microsoft.Maui.Controls.Xaml;
[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
Pri trimmingu narazíte na dva typy chýb. Prvý je IL2026 / IL2104 warning, keď linker hlási, že nevie staticky určiť, ktoré typy potrebujete kvôli reflexii. Riešenie sú anotácie [DynamicallyAccessedMembers] alebo explicitné TrimmerRootDescriptor XML. Druhý je tichý runtime crash, typicky MissingMethodException pri JSON deserializácii. Pre System.Text.Json používajte JsonSerializerContext so source generátorom, čím sa úplne vyhnete reflexii. Pre praktický príklad volania API odolného voči trimmingu pozrite sprievodcu HttpClient a servisnou vrstvou.
// AOT-friendly JSON deserializácia
[JsonSerializable(typeof(UserDto))]
[JsonSerializable(typeof(List<UserDto>))]
internal partial class AppJsonContext : JsonSerializerContext { }
// Použitie v servise
var user = JsonSerializer.Deserialize(
json,
AppJsonContext.Default.UserDto);
Ako nájsť memory leaks v .NET MAUI
Memory leak v MAUI takmer vždy znamená, že Page alebo BindingContext ostáva v pamäti po tom, ako navigujete preč. Najčastejší vinník? Event handler registrovaný v page constructore alebo v OnAppearing, ktorý sa neodhlasuje v OnDisappearing. MAUI Shell drží silné referencie na predošlé stránky cez navigačnú históriu, takže každý leak sa znásobuje. Tento bug som naposledy odhalil v jednom projekte minulý kvartál, keď nám pamäť za hodinu používania narástla z 80 MB na 410 MB.
Diagnostiku robím v dvoch krokoch. Najprv WeakReference test, ktorý odhalí, či sa stránka vôbec garbage-collectuje:
// V test projekte alebo v debug builde
[Fact]
public async Task ProductsPage_DoesNotLeak()
{
WeakReference pageRef;
{
var page = new ProductsPage(new ProductsViewModel());
pageRef = new WeakReference(page);
await page.Navigation.PushAsync(page);
await page.Navigation.PopAsync();
}
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Assert.False(pageRef.IsAlive,
"ProductsPage ostal v pamäti, pravdepodobne neodhlásený event handler.");
}
Druhý krok je heap snapshot. Vo Visual Studio 2026 spustite app v Release configu, otvorte Debug → Performance Profiler → .NET Object Allocation Tracking, urobte snapshot pred a po navigácii a porovnajte počet inštancií typov ContentPage, BindableObject a vašich VM-ov. Ak po pop-ach počet rastie, máte leak.
Najčastejšie zdroje leakov, ktoré som videl v produkcii:
Statický event: Connectivity.ConnectivityChanged += OnChanged v page constructore bez odhlásenia.
MessagingCenter / IMessenger bez Unregister: hoci WeakReferenceMessenger z CommunityToolkit.Mvvm tento problém rieši (preferujte ho).
Long-lived service drží Page: napríklad lokalizačný service so silnou referenciou na VisualElement.
Timer alebo CancellationTokenSource: System.Threading.Timer drží callback, kým ho nezdisposujete.
Optimalizácia CollectionView, obrázkov a Shell navigácie
CollectionView v MAUI 10 používa natívne RecyclerView (Android) a UICollectionView (iOS), takže virtualizácia funguje out-of-the-box. Ale iba ak ju nesabotujete. Tri pravidlá, ktoré opakujem v každom code review:
Fixed height items: kdekoľvek to dáva zmysel, nastavte ItemSizingStrategy="MeasureFirstItem". Pri dynamickej výške renderer meria každú položku zvlášť, čo zabíja scroll fps.
DataTemplateSelector hot path: OnSelectTemplate sa volá pri každom rebinde. Žiadny LINQ, žiadne typeof() porovnania. Používajte switch na enum alebo discriminator property.
Incrementálny load: RemainingItemsThreshold="5" a RemainingItemsThresholdReachedCommand miesto načítavania celého zoznamu naraz.
Pri obrázkoch je najväčšia výhra prepnutie z Image Source="https://..." na knižnicu so správnym diskovým a memory cache. Osobne odporúčam FFImageLoading.Maui alebo UraniumUI.MaterialIcons pre vektorové ikony. Statické obrázky vždy poskytujte cez MauiAsset ako vektory (.svg → .png resampling robí MAUI build automaticky pre všetky DPI).
Shell navigácia má skrytú pascu. FlyoutItem a všetky Tab stránky sa inštanciujú už pri starte, nielen pri zobrazení. Použite Shell.PresentationMode="ModalAnimated" a lazy registráciu cez Routing.RegisterRoute pre stránky, ktoré nemajú byť v hlavnej navigácii. Pri praktickom nastavovaní notifikácií a deep linkov sa tieto trade-offy prelínajú s tým, čo opisujeme v článku o push notifikáciách v .NET MAUI 10.
Profilovanie pomocou dotnet-trace a PerfView
Pre detailný startup trace na Androide použite dotnet-trace v kombinácii s Android profilerom. Najprv inštalujte nástroj globálne, potom zachyťte trace v okamihu, keď app štartuje. Kompletná dokumentácia nástroja je v oficiálnej Microsoft .NET diagnostics príručke.
Výsledný .speedscope.json otvoríte na speedscope.app a vidíte flame graph CPU času. Hľadajte široké pásy s prefixom Microsoft.Maui alebo MyApp, to sú vaše hot paths. V mojich projektoch obvykle dominujú tri kategórie: DI resolution, XAML inflation a JSON deserialization.
Pre memory profile použite Visual Studio Diagnostic Tools alebo dotnet-gcdump:
dotnet-gcdump collect --process-id <PID> --type heap
# Vygeneruje *.gcdump súbor, otvorte v Visual Studio
Komplementárne pre Android použite Android Studio Memory Profiler attachnutý cez ADB. Ten vám ukáže aj natívnu pamäť alokovanú Java vrstvou a Skia rendererom, ktoré dotnet-gcdump nevidí.
Produkčný checklist pred releasom
Pred každým release buildom prejdem tento zoznam. Ušetril mi viac než jednu zlú recenziu v Google Play:
✅ Configuration=Release a PublishTrimmed=true v CI
✅ RunAOTCompilation=true a AndroidEnableProfiledAot=true pre Android
✅ PublishAot=true pre iOS / Mac Catalyst (alebo aspoň UseInterpreter=false)
✅ XAML compilation aktívna naprieč všetkými assemblies
✅ Žiadne synchrónne IO v MauiProgram, App alebo prvom OnAppearing
✅ Všetky page event handlery odhlásené v OnDisappearing alebo cez IDisposable
✅ WeakReferenceMessenger namiesto MessagingCenter
✅ CollectionView má fixed item size alebo aspoň ItemSizingStrategy="MeasureFirstItem"
✅ Obrázky cez MauiImage SVG → multi-DPI export, nie raw .png
✅ Startup trace zachytený a porovnaný s minulým releasom (žiadna regresia > 10 %)
✅ Heap snapshot, žiadny ContentPage ani VM nesmie pretrvať po navigácii preč
✅ Crash-free rate > 99,5 % na predprodukčnom canary build (App Center / Firebase Crashlytics)
Tieto kroky doplnia obrázok kvality, ktorý začína architektúrou (MVVM, DI) a končí pri tom, ako sa aplikácia reálne správa v rukách používateľa. Výkon nie je jednorazová úloha, je to disciplína, ktorú zavádzate do CI a do PR review, aby regresia nemala kde vzniknúť.
Často kladené otázky
Funguje Native AOT v .NET MAUI 10 aj pre Android?
Pre Android je Native AOT v MAUI 10 v stave preview a vyžaduje target net10.0-android s android-arm64. V produkcii odporúčam stále Profiled AOT (RunAOTCompilation=true a AndroidEnableProfiledAot=true), ktorý je stabilný a poskytuje 25 – 35 % zlepšenie cold startu oproti Monu. Full Native AOT pre Android sa očakáva ako stable v .NET 11 v novembri 2026.
Ako rýchlo by sa mala spustiť moja MAUI aplikácia?
Google Play definuje "slow cold start" ako čas nad 5 sekúnd, ale ambiciózny cieľ je pod 1,5 sekundy na mid-range Androide a pod 1 sekundu na iPhone 13+. Apple v App Store Review Guidelines očakáva, že prvá interaktívna obrazovka sa zobrazí do 2 sekúnd, inak môže byť aplikácia zamietnutá.
Prečo moja MAUI aplikácia zaberá toľko pamäte?
Tri najčastejšie príčiny: (1) všetky FlyoutItem a Tab stránky sa inštancujú pri starte Shell, používajte lazy Routing.RegisterRoute; (2) cache obrázkov nemá limit, nastavte ImageSource.CacheValidity; (3) memory leaky cez statické eventy. Heap snapshot vo Visual Studio profileri ukáže, ktorý typ dominuje.
Aký je rozdiel medzi trimming a AOT v .NET MAUI?
Trimming odstraňuje nepoužitý IL kód z výsledných assemblies, čím sa zmenšuje veľkosť aplikácie. AOT (Ahead-Of-Time) kompilácia prekladá IL do natívneho kódu už pri publishi, takže sa pri spustení nevolá JIT. V produkcii potrebujete oboje: trimming pre veľkosť, AOT pre rýchlosť. Bez trimming-u AOT generuje obrovské binárky, bez AOT trimming síce zmenší app, ale štart ostáva pomalý.
Môžem profilovať .NET MAUI aplikáciu bez Visual Studia?
Áno. Cross-platform nástroje dotnet-trace, dotnet-gcdump a dotnet-counters fungujú z príkazového riadku na Windows, macOS aj Linuxe. Výsledné súbory otvoríte v Speedscope (online), PerfView (Windows) alebo priamo v JetBrains Rider, ktorý má integrovaný dotTrace pre MAUI projekty od verzie 2025.3.
Praktický návod, ako nastaviť push notifikácie v .NET MAUI 10: Firebase Cloud Messaging pre Android aj iOS, APNs s P8 kľúčmi, deep linking cez Shell, odosielanie z .NET backendu cez FirebaseAdmin SDK a lokálne pripomienky.
Praktický sprievodca implementáciou OAuth2, OpenID Connect a JWT tokenov v .NET MAUI 10. SecureStorage, refresh tokeny, biometria a HTTP message handler v jednej kompletnej príručke.
Naučte sa správne integrovať REST API v .NET MAUI — od HttpClientFactory cez MVVM servisnú vrstvu, platformové handlery, kontrolu pripojenia až po retry a circuit breaker vzory. Praktické príklady pripravené na okamžité použitie.