.NET MAUI -sovelluksen käynnistysajan optimoinnissa tärkeimmät keinot ovat AOT-käännös (Ahead-of-Time), aggressiivinen kokoonpanojen trimmaus, kylmäkäynnistyksen profilointi dotnet-trace-työkalulla sekä DI-rekisteröintien ja XAML-bindausten viilaaminen. Tuotantotasolla hyvin viritetty MAUI-sovellus käynnistyy Androidilla 700–1200 millisekunnissa ja iOS:llä alle 600 ms:ssa, kun .NET 9:n NativeAOT ja R8-pakkaus ovat käytössä. Tässä oppaassa käydään läpi konkreettiset asetukset, mittausmetodit ja yleisimmät tuotantoa kaatavat sudenkuopat, sellaisina kuin olen ne kolmessa julkaistussa sovelluksessa nähnyt.
NativeAOT on iOS:llä oletus, mutta Androidilla LLVM-AOT + R8 antaa parhaan kompromissin koon ja nopeuden välillä .NET 9:ssä.
Trimmausmoodin nostaminen partial:sta full:iin pienentää APK:ta tyypillisesti 20–35 %, mutta vaatii reflektion käytön auditointia.
Suurin yksittäinen käynnistysajan voitto tulee usein DI-rekisteröintien laiskasta resolvoinnista, ei kääntäjäasetuksista.
XAML-bindausten kääntäminen (XamlCompilation + x:DataType) leikkaa sivun renderöintiajan noin puoleen verrattuna reflektiopohjaisiin bindauksiin.
Mittaa aina dotnet-trace:lla tai Android Profilerilla. Älä luota Stopwatch-pätkiin, jotka jättävät natiivin alustuksen huomiotta.
Splash screen on käytännössä ainoa keino piilottaa kylmäkäynnistys käyttäjältä; suunnittele se ennen ensimmäistä julkaisua.
Mikä on hyvä käynnistysaika .NET MAUI -sovellukselle?
Realistinen tavoite vuoden 2026 keskitason laitteilla on alle yhden sekunnin kylmäkäynnistys siihen hetkeen, jossa ensimmäinen sivu on interaktiivinen. Googlen Play Vitals -mittari pitää Android-sovelluksen kylmäkäynnistystä "hitaana", jos se ylittää 5 sekuntia 75. persentiilillä. Tämä on kuitenkin hälytysraja, ei tavoite. Itse pyrin asettamaan kolme erillistä budjettia: kylmäkäynnistys (cold start) alle 1200 ms, lämmin käynnistys (warm start) alle 400 ms ja kuuma käynnistys (hot start) alle 100 ms. Nämä mitataan ensimmäisen pikselin piirtoon, ei pelkkään App.xaml.cs:n konstruktoriin.
Olen huomannut, että tiimit yliarvioivat usein, miten paljon AOT yksin auttaa. Tyypillinen optimoimaton MAUI-sovellus käynnistyy Android-puolella 2200–2800 ms:ssa. AOT + R8 pudottaa tämän noin 1500 ms:iin, mutta loput 300–600 ms voitosta tulevat DI-rekisteröinneistä, kuvaresursseista ja ensimmäisen sivun bindauksista. Optimointia kannattaa siis lähestyä kokonaisuutena, ei pelkästään kääntäjäasetuksia säätämällä.
Käynnistysajan mittaaminen oikein
Ennen kuin optimoit mitään, mittaa. Stopwatch-pätkät MauiProgram.cs:ssa ovat petollisia, koska ne aloittavat ajan vasta kun MAUI on jo käynnistynyt, eivätkä ne huomioi natiivin runtimen alustusta. Käytä alustakohtaisia työkaluja: Androidilla adb shell am start -W antaa ThisTime- ja TotalTime-arvot, jotka mittaavat prosessin käynnistymisestä ensimmäisen ruudun piirtoon.
Syvempään analyysiin käytä dotnet-trace:a, joka osaa kerätä managed-puolen profiilin myös laitteelta. Yhdistä se Perfetton tai Android Studion CPU Profilerin kanssa, niin näet sekä natiivin että managed-puolen aikajanan rinnakkain. iOS:llä Instrumentsin App Launch -malli on käytännössä ainoa luotettava työkalu (Xcode 16:sta lähtien se tunnistaa MAUI-prosessit suoraan).
Toista mittaus vähintään kymmenen kertaa ja käytä mediaania, sillä yksittäiset ajot heittelevät helposti ±20 %, kun käyttöjärjestelmä lämmittää välimuistejaan. Käynnistä laite myös täysin uudelleen jokaisten kymmenen ajon välein, jotta saat puhtaan kylmäkäynnistyksen mittauksen. Aja yksi rinnakkainen mittaus profiloidulla Release-buildilla ja yksi profiloimattomalla. Ero kertoo, kuinka paljon profilointi yksinään aiheuttaa overheadia.
Yksi käytännön vinkki: rakenna käynnistysajan mittaus CI-putkeen. Aja jokaisen Pull Requestin jälkeen adb shell am start -W -mittaus emulaattorilla, ja varoita, jos mediaani heikkenee yli 100 ms:lla. Tämä estää regressiot ennen niiden päätymistä tuotantoon. Olen ottanut tämän käyttöön kahdessa tiimissä, ja molemmissa se on löytänyt huonosti suunnitellut DI-rekisteröinnit ennen julkaisua.
AOT vs. JIT: mitä eroa MAUI:ssa on?
JIT (Just-in-Time) -kääntäjä muuntaa IL-koodin konekielelle ajon aikana. Se vähentää sovelluksen kokoa, mutta lisää käynnistysviivettä, koska metodit käännetään ensimmäisellä kutsulla. AOT (Ahead-of-Time) tekee käännöksen jo build-vaiheessa. Lopputulos on suurempi binääri, mutta nopeampi käynnistys ja deterministinen suorituskyky. iOS ei ole koskaan sallinut JIT:iä Apple Storen sääntöjen vuoksi, joten siellä AOT on aina ollut pakollinen. Androidilla valinta on sinun.
.NET 9 toi NativeAOT:n iOS:lle täysin tuettuna ja parannetun LLVM-AOT-tuen Androidille. Käytännössä trade-off on tämä:
Ominaisuus
JIT (Android)
LLVM-AOT (Android)
NativeAOT (iOS)
Käynnistysaika
Hitain
Nopea
Nopein
APK/IPA-koko
Pienin
+15–25 %
+20–30 %
Build-aika
Nopein
2–4× hitaampi
3–6× hitaampi
Reflektion tuki
Täysi
Rajallinen
Vaatii rd.xml-määrittelyt
Dynaaminen koodi
Sallittu
Sallittu
Estetty
Tuotantokypsyys
Vakaa
Vakaa (.NET 9)
Vakaa (.NET 9)
Pääsääntöisesti suosittelen Release-buildiin <RunAOTCompilation>true</RunAOTCompilation> ja Debug-buildiin JIT:iä, jotta iterointi pysyy nopeana. Lisätietoa löytyy Microsoftin NativeAOT-dokumentaatiosta MAUI:lle.
Android: LLVM-AOT, R8 ja startup tracing
Android-puolella tärkein .csproj-konfiguraatio Release-buildiin näyttää tältä:
AndroidEnableProfiledAot on usein aliarvostettu lippu. Se kääntää AOT:lla vain ne metodit, jotka esiintyvät profiilissa (loput jätetään JIT:ille). Tämä on järkevä kompromissi APK-koon ja käynnistysajan välillä. Voit kerätä oman profiilin aprofutil:lla yhdessä sovellusajossa ja viedä sen build-prosessiin.
R8 (Androidin uusi koodin shrinkkeri) pakkaa Java-puolen luokat. Varmista, että ProGuard-säännöt eivät säilytä turhia luokkia: jokainen -keep class ** -rivi tarkoittaa, että R8 ei voi poistaa kyseistä koodia. AndroidStripILAfterAOT poistaa managed-IL:n binäärista AOT-käännöksen jälkeen, mikä leikkaa APK:ta tyypillisesti 8–12 %.
iOS: NativeAOT ja linker-asetukset
iOS:llä NativeAOT on .NET 9:stä lähtien tuotantotasolla tuettu. Aktivoi se .csproj-tiedostossa:
NativeAOT:n suurin ero perinteiseen Mono-AOT:hen on, että kaikki reflektion käyttö täytyy ilmoittaa etukäteen rd.xml-tiedostossa tai DynamicDependency-attribuuteilla. Jos käytät System.Text.Jsonin source generatoria (joka on suositukseni MAUI:ssa joka tapauksessa), pääset suurelta osin pakoon manuaalista konfiguraatiota.
// Käytä source-generoituja JSON-kontekstia reflektion sijaan
[JsonSerializable(typeof(Tuote))]
[JsonSerializable(typeof(List<Tuote>))]
internal partial class SovellusJsonContext : JsonSerializerContext { }
// Käytössä
var tuotteet = JsonSerializer.Deserialize(json, SovellusJsonContext.Default.ListTuote);
Olen tehnyt tämän siirron kahdessa tuotantosovelluksessa, ja molemmissa kylmäkäynnistys parani 280–340 ms iPhone 13:lla, kun JSON-parsinta siirrettiin source-generaattorille. IPA-koko kasvoi noin 18 %, mikä on hyväksyttävä kustannus useimmille B2C-sovelluksille.
NativeAOT:n linker-asetuksissa MtouchLink=Full on aggressiivisempi kuin SdkOnly: se trimmaa myös käyttäjäkoodin kokoonpanot. Tämä on suositeltavaa, mutta lisää regressiotestauksen tarvetta. Aja vähintään smoke-testit jokaisella nimetyllä build-konfiguraatiolla, jotta huomaat hiljaisesti poistetut metodit ennen App Storen toimitusta. IlcOptimizationPreference=Speed -asetus puolestaan suosii suoritusnopeutta binäärin koon yli. Vaihtoehto on Size, joka on järkevämpi, jos sovellus on lähellä App Storen tiedoston kokorajoja.
Kokoonpanojen trimmaus tuotannossa
Trimmaus poistaa käyttämättömän koodin kokoonpanoista build-aikana. .NET 9:ssä on kolme käytännön moodia: partial (oletus MAUI:lle), full ja copyused. Suurin osa MAUI-projekteista hyötyy siirtymästä full:iin, mutta se vaatii työtä. Aloita näin:
SuppressTrimAnalysisWarnings=false on tässä avainasemassa. Se nostaa esiin kaikki IL2026- ja IL2104-varoitukset, jotka kertovat reflektiosta, jota trimmari ei voi seurata. Käy nämä yksitellen läpi, lisää DynamicDependency-attribuutit tai vaihda source generatorin käyttöön. Yksi varoitus ohitettuna tuotannossa on yksi MissingMethodException käyttäjälle.
Jos käytät MAUI-sovelluksessasi taustapalvelua REST-rajapintoihin, suosittelen tutustumaan myös .NET MAUI REST API -oppaaseen HttpClientillä ja DI:llä, jossa käyn läpi konkreettiset typed clientit, jotka pelaavat hyvin trimmauksen kanssa.
DI-rekisteröinnit ja MauiProgram-optimointi
Tyypillinen virhe, jonka näen koodikatselmuksissa: kaikki palvelut rekisteröidään AddSingleton-puhelulla ja resolvoidaan App.xaml.cs:n konstruktorissa. Tämä tarkoittaa, että jokainen palvelu instantsoituu ennen ensimmäisen sivun piirtoa, myös ne, joita käyttäjä ei tarvitse käynnistyksen ensimmäisen 3 sekunnin aikana.
// HUONO: kaikki palvelut välittömästi instantsoituvat
builder.Services.AddSingleton<ITuotePalvelu>(sp =>
new TuotePalvelu(sp.GetRequiredService<HttpClient>())); // kallis konstruktori
// HYVÄ: laiska tehdas, instantsoituu vasta kun tarvitaan
builder.Services.AddSingleton<Lazy<ITuotePalvelu>>(sp =>
new Lazy<ITuotePalvelu>(() => new TuotePalvelu(sp.GetRequiredService<HttpClient>())));
// ViewModelissa
public TuoteSivuViewModel(Lazy<ITuotePalvelu> palvelu) => _palvelu = palvelu;
private async Task LataaTuotteet() => _tuotteet = await _palvelu.Value.HaeKaikki();
Toinen iso voitto: rekisteröi sivut ja ViewModelit AddTransient:lla, mutta vältä konstruktoriparametreissä raskaita riippuvuuksia. Jos ViewModelin konstruktori tekee verkkokutsun, käyttäjä odottaa sitä jokaisella navigoinnilla. Käytä OnAppearing-tapahtumaa tai IAsyncInitializable-kuviota raskaaseen alustukseen.
Shell-navigoinnin oppaassa käyn läpi tarkemmin, miten Shellin reititys ja DI yhdistyvät. Tämä on tärkeää, koska Shell instansioi sivut lazy-ladaten oletuksena, mutta vain jos rekisteröit ne oikein.
XAML-käännös ja kompiloidut bindaukset
XAML kompiloidaan oletuksena .NET MAUI:ssa, mutta bindausten kompilointi vaatii x:DataType-määrittelyn jokaiselle datayhteysjuurille. Ilman tätä bindaukset ratkotaan reflektiolla ajon aikana, mikä on hidasta sekä käynnistyksessä että sivulla siirtymässä.
Lisää <MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation> ja <NoWarn>XC0022;XC0023</NoWarn> EI ole oikea lähestymistapa, koska nuo varoitukset kertovat juuri niistä bindauksista, jotka ratkotaan reflektiolla. Korjaa varoitukset, älä piilota niitä.
Splash screen ja koettu nopeus
Mikään optimointi ei tee MAUI-sovelluksesta välitöntä. Kylmäkäynnistyksessä natiivin runtimen alustus, AOT-koodin lataus ja ensimmäisen sivun rendaus vievät aina vähintään 400–600 ms keskitason laitteella. Splash screen on ainoa tapa piilottaa tämä käyttäjältä.
.NET 8:sta lähtien MAUI tukee staattista splash screeniä, joka renderöityy natiivisti ennen kuin .NET-runtime edes käynnistyy. Konfigurointi .csproj-tiedostossa:
Käytä SVG-tiedostoa, sillä se skaalautuu automaattisesti kaikille tiheyksille ilman raster-resurssien tuottamisen vaivaa. Vältä animaatioita splash screenillä; ne pakottavat lisäalustusta natiivipuolelle ja nostavat oikeasti mitattua käynnistysaikaa, vaikka koettu nopeus tuntuisi paremmalta.
Lisäksi: älä koskaan tee verkkokutsuja tai tietokantakyselyitä App.xaml.cs:n konstruktorissa tai OnStart-metodissa. Siirrä ne ensimmäisen sivun OnAppearing-hetkeen. Käytännössä tämä tarkoittaa, että voit hyödyntää sovelluksesi alkutilan välimuistia, ja käyn tämän kuvion läpi tarkemmin SQLite-paikallisen tietokannan oppaassa.
Yleiset sudenkuopat tuotannossa
Olen kerännyt tähän listan ongelmista, jotka olen nähnyt toistuvasti tuotannossa. Nämä eivät näy emulaattorissa, mutta tappavat suorituskyvyn oikealla laitteella.
Suuret kuvat App-resursseissa. Jokainen Resources/Images-kansion PNG ladataan muistiin ennen ensimmäisen sivun rendausta. Käytä WebP-formaattia ja pakkaa kuvat alle 100 kB:hen.
Useat ContentView-tasot. Jokainen sisäkkäinen näkymä kuluttaa layout-aikaa. Käytä Grid:iä mieluummin kuin syvää StackLayout-hierarkiaa.
Globaalit tyylit App.xaml:ssä. Iso resurssisanakirja kasvattaa käynnistysaikaa lineaarisesti. Jaa tyylit erillisiin sanakirjoihin ja yhdistä ne tarvittaessa.
Liian aikainen tietokannan migraatio. Älä aja db.Migrate()-kutsua synkronisesti MauiProgram.CreateMauiApp:ssa. Siirrä se taustasäikeeseen ja näytä splash screen sen ajaksi.
Application Insights ja telemetria. SDK:n alustus voi viedä 100–200 ms. Lykkää sitä Task.Run:iin, jotta se ei estä käyttöliittymän rendausta.
Fonttien lataus käynnistyksessä. Jokainen ConfigureFonts-kutsulla rekisteröity custom-fontti ladataan ja parsitaan ennen ensimmäistä rendausta. Pidä määrä alle kolmessa ja käytä subset-fontteja, joissa on vain käytetyt glyfit.
Hot Reload jätetty päälle Releasessa. Olen nähnyt tämän yllättävän usein: EnableHotReload-lippu unohtuu .csproj:hen, mikä lisää käynnistykseen huomattavan diagnostiikkaketjun. Tarkista aina Release-konfiguraatio erikseen.
Honestly, näistä yhdeksästä viimeinen on se, jota itse en olisi koskaan uskonut näkeväni tuotannossa, ennen kuin törmäsin siihen kolmen eri tiimin koodissa. Lisätietoa virallisesta puolesta löytyy .NET MAUI -tiimin suorituskyky-FAQ:sta GitHubissa, jota päivitetään säännöllisesti uusimpien runtime-muutosten mukaan.
Usein kysyttyä
Kuinka kauan .NET MAUI -sovelluksen pitäisi kestää käynnistyä?
Tuotantotasolla optimoidun MAUI-sovelluksen kylmäkäynnistys keskitason Android-laitteella on 700–1200 ms ja iOS:llä alle 600 ms. Yli 2 sekunnin käynnistys keskitason laitteella indikoi optimoinnin puutetta, yleensä joko JIT:n käyttöä Release-buildissa tai DI-rekisteröintien aggressiivista resolvointia.
Miksi MAUI-sovellukseni käynnistys on hidas vain laitteella, ei emulaattorissa?
Emulaattori jakaa hostin CPU:n ja ohittaa ART:n suoritusoptimoinnit. Oikea laite (etenkin halvempi ARM-keskitason) paljastaa AOT:n puutteen, suuret APK-tiedostot ja synkroniset I/O-kutsut. Mittaa aina edustavalla laitteella, ei pelkästään emulaattorilla.
Pitäisikö minun käyttää NativeAOT vai LLVM-AOT?
iOS:llä NativeAOT on .NET 9:n oletus ja paras valinta. Androidilla LLVM-AOT on tällä hetkellä vakaampi ja tuottaa pienempiä binäärejä. NativeAOT Androidille on tulossa, mutta vielä vuonna 2026 se on esikatselutilassa eikä sovellu tuotantoon.
Kuinka mittaan .NET MAUI -sovelluksen käynnistysaikaa?
Androidilla käytä adb shell am start -W -komentoa, joka antaa TotalTime-arvon prosessin alusta ensimmäisen ruudun piirtoon. iOS:llä Xcoden Instruments-työkalun App Launch -malli on luotettavin. Älä luota Stopwatch-pätkiin, sillä ne aloittavat ajan vasta managed-puolen käynnistyttyä.
Trimmaus on turvallinen, jos otat TrimAnalyzer:n käyttöön ja korjaat kaikki IL2026/IL2104-varoitukset. Suurin riski on hiljainen reflektion käyttö kolmannen osapuolen kirjastossa, ja siksi Release-buildi täytyy testata oikealla laitteella ennen julkaisua, ei pelkästään yksikkötesteissä.
Push-ilmoitukset .NET MAUI -sovelluksessa vaativat sekä Firebase FCM:n Androidille että APNs:n iOS:lle. Tässä käytännön oppaassa käydään läpi HTTP v1 -rajapinta, token-rekisteröinti, notification channels, deep linking Shell-reitille ja tuotannon kompastuskivet.