.NET MAUI -sovelluksen käynnistysajan optimointi: AOT, trimmaus ja suorituskyky tuotannossa

Optimoi .NET MAUI -sovelluksen käynnistysaika tuotannossa: AOT-käännös, kokoonpanojen trimmaus, DI-rekisteröinnit ja mittaustyökalut konkreettisin esimerkein.

.NET MAUI Käynnistysaika: AOT-opas (2026)

Päivitetty: 17.6.2026

.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.

# Mittaa Android-sovelluksen kylmäkäynnistys
adb shell am force-stop com.yritys.sovellus
adb shell am start -W com.yritys.sovellus/.MainActivity

# Tulos:
# Status: ok
# Activity: com.yritys.sovellus/.MainActivity
# ThisTime: 842
# TotalTime: 842
# WaitTime: 856

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).

# Kerää managed-startup-profiili laitteelta
dotnet-trace collect --process-id $(adb shell pidof com.yritys.sovellus) \
  --providers Microsoft-DotNETCore-SampleProfiler \
  --duration 00:00:05 \
  --output startup.nettrace

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ä:

OminaisuusJIT (Android)LLVM-AOT (Android)NativeAOT (iOS)
KäynnistysaikaHitainNopeaNopein
APK/IPA-kokoPienin+15–25 %+20–30 %
Build-aikaNopein2–4× hitaampi3–6× hitaampi
Reflektion tukiTäysiRajallinenVaatii rd.xml-määrittelyt
Dynaaminen koodiSallittuSallittuEstetty
TuotantokypsyysVakaaVakaa (.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ä:

<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)' == 'Release|net9.0-android'">
  <RunAOTCompilation>true</RunAOTCompilation>
  <EnableLLVM>true</EnableLLVM>
  <AndroidEnableProfiledAot>true</AndroidEnableProfiledAot>
  <AndroidAotProfiles Include="custom.aprof" />
  <AndroidLinkMode>SdkOnly</AndroidLinkMode>
  <AndroidPackageFormat>aab</AndroidPackageFormat>
  <AndroidEnableMarshalMethods>true</AndroidEnableMarshalMethods>
  <AndroidStripILAfterAOT>true</AndroidStripILAfterAOT>
</PropertyGroup>

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:

<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)' == 'Release|net9.0-ios'">
  <PublishAot>true</PublishAot>
  <MtouchLink>Full</MtouchLink>
  <UseInterpreter>false</UseInterpreter>
  <IlcOptimizationPreference>Speed</IlcOptimizationPreference>
  <StripSymbols>true</StripSymbols>
</PropertyGroup>

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:

<PropertyGroup>
  <PublishTrimmed>true</PublishTrimmed>
  <TrimMode>full</TrimMode>
  <SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
  <EnableTrimAnalyzer>true</EnableTrimAnalyzer>
</PropertyGroup>

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ä.

<ContentPage x:Class="Sovellus.Views.TuoteSivu"
             xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:vm="clr-namespace:Sovellus.ViewModels"
             x:DataType="vm:TuoteSivuViewModel">
    <CollectionView ItemsSource="{Binding Tuotteet}">
        <CollectionView.ItemTemplate>
            <DataTemplate x:DataType="vm:Tuote">
                <Label Text="{Binding Nimi}" />
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
</ContentPage>

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:

<MauiSplashScreen Include="Resources\Splash\splash.svg"
                  Color="#1A1A2E"
                  BaseSize="128,128" />

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ä.

Nostaako kokoonpanojen trimmaus käyttäjäkokemusta riskeerätyksi?

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ä.

Marcus Chen
Tietoa Kirjoittajasta Marcus Chen

Senior mobile architect with a decade of cross-platform experience. Spent the last five years going deep on .NET MAUI in production.