Ydeevneoptimering i .NET MAUI: Sådan Bygger Du Hurtigere Apps på Alle Platforme

Komplet guide til ydeevneoptimering i .NET MAUI med konkrete kodeeksempler. Dækker opstartstider, compiled bindings, CollectionView-virtualisering, hukommelsesstyring, asynkron programmering og profileringsværktøjer til hurtigere cross-platform apps.

Hvorfor ydeevne er alt i mobilapps

Lad os starte med en hård sandhed: brugere er utålmodige. Virkelig utålmodige. Undersøgelser viser, at en mobilapp der tager mere end 3 sekunder om at starte, mister op til 53% af sine brugere. Det er over halvdelen — væk, bare sådan.

Så ydeevne er ikke bare et teknisk mål. Det er forretningskritisk.

.NET MAUI er modnet markant siden lanceringen. Med .NET 9 og de tidlige previews af .NET 10 har Microsoft investeret massivt i ydeevneforbedringer: Native AOT-kompilering, forbedret trimming, hurtigere Shell-navigation og optimeret rendering. Men frameworket kan kun gøre halvdelen af arbejdet. Den anden halvdel? Den ligger hos dig som udvikler.

I denne guide dækker vi alt hvad du behøver at vide om ydeevneoptimering i .NET MAUI — fra opstartstider og hukommelsesforbrug til UI-rendering, databinding og netværkskald. Der er konkrete kodeeksempler og profileringsværktøjer med, så du kan bygge apps der faktisk føles hurtige og responsive på tværs af iOS, Android, Windows og macOS.

Opstartstid: Det første indtryk tæller

Opstartstiden er den mest synlige ydeevneparameter for enhver mobilapp. Det er bogstaveligt talt det allerførste brugeren oplever, og det sætter tonen for hele oplevelsen. En langsom opstart signalerer en langsom app — uanset hvor hurtig resten faktisk er.

Forstå opstartsprocessen

Når en .NET MAUI-app starter, sker der en hel række ting: runtime-initialisering, dependency injection-container opbygning, Shell-konfiguration, hovedsideindlæsning og eventuel datahentning. Hver af disse faser kan optimeres.

Det vigtigste princip er faktisk ret simpelt: gør så lidt som muligt under opstart. Alt der ikke er strengt nødvendigt for at vise den første side, bør udskydes til senere.

// ❌ Dårligt: Tung initialisering i MauiProgram.cs
public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder.UseMauiApp<App>();
    
    // Undgå tunge synkrone operationer her
    var config = LoadConfigurationFromFile(); // Blokerer opstart
    var cache = PreloadAllCachedData();       // Unødvendig ved opstart
    
    return builder.Build();
}

// ✅ Bedre: Minimal opstart med udskudt initialisering
public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder.UseMauiApp<App>();
    
    // Registrer services som singletons med lazy initialisering
    builder.Services.AddSingleton<IConfigService, ConfigService>();
    builder.Services.AddSingleton<ICacheService, CacheService>();
    
    // Konfiguration indlæses først når den faktisk bruges
    return builder.Build();
}

Dependency Injection med omtanke

Dependency injection (DI) er en hjørnesten i moderne .NET-udvikling, men den har en omkostning. Hver gang en service resolves, bruger DI-containeren reflection til at oprette instansen og dens afhængigheder. Med dybe afhængighedstræer kan det faktisk blive ret mærkbart.

// ✅ Brug Singleton for services der ikke ændrer sig
builder.Services.AddSingleton<IApiService, ApiService>();
builder.Services.AddSingleton<ISettingsService, SettingsService>();

// ✅ Brug Transient kun for services der SKAL oprettes hver gang
builder.Services.AddTransient<IFormValidator, FormValidator>();

// ❌ Undgå at registrere Scoped services medmindre det er nødvendigt
// I MAUI er Scoped-livstiden ofte misforstået og kan føre til
// uventede hukommelseslækager

En teknik jeg selv bruger en del er Lazy<T> til services der er tunge at oprette men sjældent bruges:

// Registrer en lazy-indlæst service
builder.Services.AddSingleton<Lazy<IAnalyticsService>>(sp =>
    new Lazy<IAnalyticsService>(() => sp.GetRequiredService<IAnalyticsService>()));

// I din ViewModel
public class MainViewModel
{
    private readonly Lazy<IAnalyticsService> _analytics;
    
    public MainViewModel(Lazy<IAnalyticsService> analytics)
    {
        _analytics = analytics;
        // Analytics-servicen oprettes IKKE endnu
    }
    
    public void TrackEvent(string eventName)
    {
        // Først her oprettes servicen
        _analytics.Value.Track(eventName);
    }
}

AOT-kompilering og trimming

Ahead-of-Time (AOT) kompilering er ærligt talt en af de mest effektive måder at forbedre opstartstiden på. I stedet for at kompilere IL-kode til maskinkode ved runtime (JIT), gør AOT det på byggetidspunktet. Resultatet er typisk op til 2x hurtigere opstart og op til 2,5x mindre appstørrelse. Det er virkelig imponerende tal.

Med .NET 9 blev Native AOT eksperimentelt understøttet for iOS og Mac Catalyst. I .NET 10 er understøttelsen udvidet, selvom Android-support stadig er under udvikling.

<!-- I din .csproj-fil -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
    <!-- Aktiver fuld trimming -->
    <PublishTrimmed>true</PublishTrimmed>
    <TrimMode>full</TrimMode>
    
    <!-- Aktiver AOT for iOS/Mac -->
    <PublishAot>true</PublishAot>
    
    <!-- Aktiver interpreteret kode som fallback -->
    <UseInterpreter>true</UseInterpreter>
</PropertyGroup>

Vigtigt: Fuld trimming fjerner kode der ikke refereres statisk. Det betyder at reflection-baseret kode kan gå i stykker. Sørg for at teste grundigt med trimming aktiveret, og brug [DynamicallyAccessedMembers]-attributter hvor nødvendigt for at bevare typer. Det har fanget mig mere end én gang.

Compiled Bindings: Slut med reflection i databinding

Databinding er fundamentalt i enhver MAUI-applikation, men her er problemet med traditionelle string-baserede bindings: de bruger reflection ved runtime for at finde properties. Det er langsomt, og det kan føre til runtime-fejl der først opdages når brugeren rammer den pågældende side. Ikke optimalt.

Compiled bindings løser begge problemer. De kompileres på byggetidspunktet, giver compile-time fejlkontrol og eliminerer reflection-overhead.

<!-- ❌ Traditionel binding med string (bruger reflection) -->
<Label Text="{Binding UserName}" />

<!-- ✅ Compiled binding med x:DataType -->
<ContentPage xmlns:vm="clr-namespace:MinApp.ViewModels"
             x:DataType="vm:ProfilViewModel">
    <Label Text="{Binding UserName}" />
    <Label Text="{Binding Email}" />
    <Button Command="{Binding GemProfilCommand}" />
</ContentPage>

Med x:DataType angivet på siden (eller en container) ved compileren præcis hvilken type der bindes til. Hvis du skriver {Binding UserNaem} — en stavefejl — får du en kompileringsfejl i stedet for en tavs fejl ved runtime. Det alene er guld værd.

Compiled bindings i lister

Ydeevnegevinsten ved compiled bindings er mest mærkbar i lister som CollectionView og ListView, hvor hundredvis af binding-evalueringer kan ske under scrolling:

<CollectionView ItemsSource="{Binding Produkter}">
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="models:Produkt">
            <Grid Padding="10" ColumnDefinitions="60,*,Auto">
                <Image Source="{Binding BilledeUrl}"
                       WidthRequest="50"
                       HeightRequest="50" />
                <VerticalStackLayout Grid.Column="1" Spacing="4">
                    <Label Text="{Binding Navn}"
                           FontAttributes="Bold" />
                    <Label Text="{Binding Beskrivelse}"
                           MaxLines="2"
                           FontSize="12" />
                </VerticalStackLayout>
                <Label Grid.Column="2"
                       Text="{Binding Pris, StringFormat='{0:C}'}"
                       VerticalOptions="Center" />
            </Grid>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

Fra .NET 9 og fremefter er compiled bindings påkrævet i apps der bruger Native AOT eller fuld trimming. Så det er en god idé at begynde migreringen allerede nu — du skal alligevel den vej.

CollectionView og listeydeevne

Lister er et af de mest almindelige UI-mønstre i mobilapps — og desværre også et af de mest problematiske ydeevneområder. CollectionView er MAUIs moderne listevisning med indbygget virtualisering, men det kræver korrekt brug for at fungere godt.

Virtualiseringens guldregler

Virtualisering betyder at kun de synlige elementer (plus et lille buffer) oprettes og renderes. Når brugeren scroller, genbruges eksisterende celler med nyt data. Men denne mekanisme kan nemmere end man tror brydes:

<!-- ❌ ALDRIG gør dette: CollectionView inde i ScrollView -->
<ScrollView>
    <VerticalStackLayout>
        <Label Text="Overskrift" />
        <CollectionView ItemsSource="{Binding Items}" />
    </VerticalStackLayout>
</ScrollView>
<!-- ScrollView tvinger CollectionView til at rendere ALLE elementer -->

<!-- ✅ Korrekt: Brug Grid med stjernerækker -->
<Grid RowDefinitions="Auto,*">
    <Label Text="Overskrift" />
    <CollectionView Grid.Row="1"
                    ItemsSource="{Binding Items}" />
</Grid>
<!-- CollectionView har en begrænset højde og virtualiserer korrekt -->

Her er de vigtigste regler du bør følge:

  • Placér altid CollectionView i en Grid med stjernerække (*) — det giver den en begrænset højde, som er nødvendig for at virtualisering virker.
  • Undgå at pakke CollectionView ind i ScrollView eller StackLayout — begge fjerner den højdebegrænsning der aktiverer virtualisering. Jeg har set dette fejlmønster i utallige projekter.
  • Hold DataTemplate simpelt — komplekse, dybt indlejrede layouts i hver celle forsinker rendering mærkbart.
  • Brug en fast cellehøjde når muligt — det eliminerer behovet for at måle hver celle dynamisk.

Inkrementel indlæsning

For store datasæt bør du aldrig indlæse alle data på én gang. Brug i stedet inkrementel indlæsning (også kaldet uendelig scrolling):

public class ProdukterViewModel : ObservableObject
{
    private readonly IProduktService _produktService;
    private int _currentPage = 0;
    private const int PageSize = 20;
    
    public ObservableCollection<Produkt> Produkter { get; } = new();
    
    public IAsyncRelayCommand IndlæsFlereCommand { get; }
    
    public ProdukterViewModel(IProduktService produktService)
    {
        _produktService = produktService;
        IndlæsFlereCommand = new AsyncRelayCommand(IndlæsFlere);
    }
    
    private async Task IndlæsFlere()
    {
        _currentPage++;
        var nyttData = await _produktService
            .HentProdukterAsync(_currentPage, PageSize);
        
        foreach (var produkt in nyttData)
        {
            Produkter.Add(produkt);
        }
    }
}
<CollectionView ItemsSource="{Binding Produkter}"
                RemainingItemsThreshold="5"
                RemainingItemsThresholdReachedCommand="{Binding IndlæsFlereCommand}">
    <!-- DataTemplate her -->
</CollectionView>

Med RemainingItemsThreshold sat til 5 vil kommandoen udløses når brugeren er 5 elementer fra bunden. Det giver en sømløs scrolloplevelse — brugeren mærker slet ikke at der indlæses nyt data.

Hukommelsesstyring og lækageforebyggelse

Hukommelseslækager er en af de mest udbredte (og frustrerende) årsager til dårlig ydeevne i mobilapps. En lækage betyder at objekter ikke frigives fra hukommelsen, selv når de ikke længere bruges. Over tid fører det til stigende hukommelsesforbrug, langsom scrolling, og i værste fald — app-nedbrud.

De mest almindelige lækagekilder

I .NET MAUI er de hyppigste syndere:

  1. Event handlers der ikke afregistreres: Når du abonnerer på en hændelse (f.eks. Clicked, Appearing), holder udgiveren en reference til abonnenten. Glemmer du at afregistrere, kan abonnenten aldrig garbage collectes.
  2. Stærke referencer i custom renderers: Platform-specifikke handlers der holder referencer til MAUI-elementer kan forhindre garbage collection.
  3. Billeder der ikke disposes: Store billeder der ikke frigives korrekt fylder hurtigt hukommelsen op.
  4. Statiske event handlers og singletons med sidereferencer: Statiske referencer lever lige så længe som applikationen selv.
// ❌ Hukommelseslækage: Event handler afregistreres aldrig
public partial class MinSide : ContentPage
{
    protected override void OnAppearing()
    {
        base.OnAppearing();
        MessagingCenter.Subscribe<App, string>(this, "Besked", 
            (sender, msg) => HandleBesked(msg));
    }
    // OnDisappearing mangler unsubscribe!
}

// ✅ Korrekt: Afregistrer event handlers
public partial class MinSide : ContentPage
{
    protected override void OnAppearing()
    {
        base.OnAppearing();
        MessagingCenter.Subscribe<App, string>(this, "Besked", 
            (sender, msg) => HandleBesked(msg));
    }
    
    protected override void OnDisappearing()
    {
        base.OnDisappearing();
        MessagingCenter.Unsubscribe<App, string>(this, "Besked");
    }
}

Bemærk: MessagingCenter er markeret som forældet i nyere versioner af .NET MAUI. Brug i stedet WeakReferenceMessenger fra CommunityToolkit.Mvvm — den håndterer svage referencer automatisk, hvilket er et kæmpe plus:

// ✅ Moderne tilgang med WeakReferenceMessenger
using CommunityToolkit.Mvvm.Messaging;

public partial class MinSide : ContentPage, 
    IRecipient<BeskedNotifikation>
{
    protected override void OnAppearing()
    {
        base.OnAppearing();
        WeakReferenceMessenger.Default.Register<BeskedNotifikation>(this);
    }
    
    protected override void OnDisappearing()
    {
        base.OnDisappearing();
        WeakReferenceMessenger.Default.Unregister<BeskedNotifikation>(this);
    }
    
    public void Receive(BeskedNotifikation message)
    {
        // Håndter beskeden
    }
}

// Definer beskedtypen
public record BeskedNotifikation(string Indhold);

Billedhåndtering og caching

.NET MAUI cacher downloadede billeder i 24 timer som standard. Men for apps med mange billeder — tænk e-handelsapps eller sociale medier — er standardindstillingerne sjældent tilstrækkelige.

// Konfigurer billedcaching i MauiProgram.cs
builder.Services.AddSingleton<IImageCacheService>(sp =>
{
    return new ImageCacheService(new ImageCacheOptions
    {
        CacheDirectory = FileSystem.CacheDirectory,
        MaxCacheSize = 100 * 1024 * 1024, // 100 MB maks cache
        DefaultExpiration = TimeSpan.FromDays(7),
        EnableMemoryCache = true,
        MemoryCacheCapacity = 50 // Maks 50 billeder i hukommelsen
    });
});

For bedre kontrol over billedindlæsning kan du overveje at implementere størrelsesbegrænsning (det gør en verden til forskel på enheder med begrænset RAM):

// Undgå at indlæse store billeder i fuld opløsning
public static ImageSource IndlæsOptimeretBillede(string url, 
    int maxBredde, int maxHøjde)
{
    // Brug URL-parametre til at anmode om en korrekt størrelse
    // fra serveren (hvis API'et understøtter det)
    var optimizedUrl = $"{url}?w={maxBredde}&h={maxHøjde}&fit=crop";
    
    return ImageSource.FromUri(new Uri(optimizedUrl));
}

Asynkron programmering: Hold UI-tråden fri

Den gyldne regel i mobiludvikling er simpel: blokér aldrig UI-tråden. Enhver operation der tager mere end 16 millisekunder (det svarer til én frame ved 60 fps) kan forårsage synlig hakken i brugergrænsefladen. Og brugerne mærker det — garanteret.

Asynkrone mønstre der faktisk virker

// ❌ Blokerer UI-tråden
public class ProfilViewModel
{
    public ProfilViewModel()
    {
        // Konstruktøren er synkron — kald ALDRIG async kode her
        var profil = _profilService.HentProfil().Result; // DEADLOCK-risiko!
    }
}

// ✅ Korrekt asynkron initialisering
public class ProfilViewModel : ObservableObject
{
    private readonly IProfilService _profilService;
    private bool _isLoading;
    private Profil _profil;
    
    public bool IsLoading
    {
        get => _isLoading;
        set => SetProperty(ref _isLoading, value);
    }
    
    public Profil Profil
    {
        get => _profil;
        set => SetProperty(ref _profil, value);
    }
    
    public IAsyncRelayCommand IndlæsProfilCommand { get; }
    
    public ProfilViewModel(IProfilService profilService)
    {
        _profilService = profilService;
        IndlæsProfilCommand = new AsyncRelayCommand(IndlæsProfilAsync);
    }
    
    private async Task IndlæsProfilAsync()
    {
        try
        {
            IsLoading = true;
            Profil = await _profilService.HentProfilAsync();
        }
        finally
        {
            IsLoading = false;
        }
    }
}

Udskyd tung initialisering med LazyView

For sider med komplekse views der tager tid at oprette, er LazyView fra .NET MAUI Community Toolkit din ven:

<!-- I stedet for at oprette et tungt view med det samme -->
<ContentPage xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             xmlns:views="clr-namespace:MinApp.Views">
    <Grid RowDefinitions="Auto,*">
        <Label Text="Dashboard" FontSize="24" />
        
        <!-- LazyView opretter først GrafView når det bliver synligt -->
        <toolkit:LazyView Grid.Row="1" x:TypeArguments="views:GrafView" />
    </Grid>
</ContentPage>

Shell-navigation: Hurtigere sideovergange

Shell er MAUIs anbefalede navigationsmodel, men den kan føles langsom hvis sider registreres forkert. Et af de mest udbredte problemer er, at alle sider oprettes under opstart — selv dem brugeren aldrig besøger. Det er spild af ressourcer.

// ❌ Sider oprettes ved opstart
public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();
        // Disse sider oprettes med det samme
        Items.Add(new ShellContent { Content = new ProfilPage() });
        Items.Add(new ShellContent { Content = new IndstillingerPage() });
    }
}

// ✅ Registrer ruter for lazy loading
public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();
        // Sider oprettes først når brugeren navigerer til dem
        Routing.RegisterRoute("profil", typeof(ProfilPage));
        Routing.RegisterRoute("indstillinger", typeof(IndstillingerPage));
        Routing.RegisterRoute("produktdetaljer", typeof(ProduktDetaljerPage));
    }
}

Navigation med parametre

// Navigér med query-parametre
await Shell.Current.GoToAsync($"produktdetaljer?id={produkt.Id}");

// Modtag parametre med attribut
[QueryProperty(nameof(ProduktId), "id")]
public partial class ProduktDetaljerPage : ContentPage
{
    private int _produktId;
    public int ProduktId
    {
        get => _produktId;
        set
        {
            _produktId = value;
            // Indlæs data asynkront
            _ = IndlæsProduktAsync(value);
        }
    }
    
    private async Task IndlæsProduktAsync(int id)
    {
        // Vis loading-indikator mens data hentes
        LoadingIndicator.IsVisible = true;
        var produkt = await _produktService.HentAsync(id);
        BindingContext = produkt;
        LoadingIndicator.IsVisible = false;
    }
}

Layout-optimering: Fladere hierarkier, hurtigere rendering

Layoutberegning er en af de mest ressourcekrævende operationer i MAUI. Hver gang et element ændrer størrelse eller position, skal hele layouttræet genberegnes. Jo dybere hierarkiet er, jo flere beregninger kræves. Det giver sig selv.

Reducer nesting

<!-- ❌ Dybt indlejret layout (5 niveauer) -->
<VerticalStackLayout>
    <Frame>
        <HorizontalStackLayout>
            <VerticalStackLayout>
                <Grid>
                    <Label Text="Dybt indlejret" />
                </Grid>
            </VerticalStackLayout>
        </HorizontalStackLayout>
    </Frame>
</VerticalStackLayout>

<!-- ✅ Fladt layout med Grid (2 niveauer) -->
<Grid ColumnDefinitions="Auto,*"
      RowDefinitions="Auto,Auto"
      Padding="10">
    <BoxView Grid.RowSpan="2"
             Color="LightGray"
             CornerRadius="8" />
    <Label Grid.Column="1" 
           Text="Fladt og hurtigt"
           Margin="10,0,0,0" />
</Grid>

Grid slår indlejrede StackLayouts

Et enkelt Grid med definerede rækker og kolonner er næsten altid hurtigere end flere indlejrede StackLayout-elementer. Grid beregner layout i én gennemgang, mens indlejrede StackLayouts kræver flere gennemgange.

Min tommelfingerregel: Hvis dit layout har mere end 3 niveauer af indlejring, bør du refaktorere til et fladere hierarki. Ingen undtagelser.

Undgå unødvendige layoutopdateringer

// ❌ Trigger layout-opdatering for hvert element
foreach (var item in items)
{
    stackLayout.Children.Add(new Label { Text = item.Navn });
    // Hvert Add() trigger en layoutberegning
}

// ✅ Brug BatchBegin/BatchEnd for at samle opdateringer
stackLayout.BatchBegin();
foreach (var item in items)
{
    stackLayout.Children.Add(new Label { Text = item.Navn });
}
stackLayout.BatchEnd();
// Kun én layoutberegning for alle elementer

Netværk og datahentning

Mobilapps er dybt afhængige af netværk, og netværkskald er typisk den langsomste del af en apps flow. God optimering af datahentning kan have en overraskende stor effekt på den oplevede ydeevne.

HttpClient best practices

// ❌ Opret ALDRIG en ny HttpClient per request
public async Task<Data> HentData()
{
    using var client = new HttpClient(); // Socket exhaustion!
    var response = await client.GetAsync("https://api.example.com/data");
    return await response.Content.ReadFromJsonAsync<Data>();
}

// ✅ Brug IHttpClientFactory
// I MauiProgram.cs
builder.Services.AddHttpClient("MinApi", client =>
{
    client.BaseAddress = new Uri("https://api.example.com/");
    client.DefaultRequestHeaders.Accept.Add(
        new MediaTypeWithQualityHeaderValue("application/json"));
    client.Timeout = TimeSpan.FromSeconds(30);
});

// I din service
public class ApiService : IApiService
{
    private readonly IHttpClientFactory _httpClientFactory;
    
    public ApiService(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }
    
    public async Task<List<Produkt>> HentProdukterAsync()
    {
        var client = _httpClientFactory.CreateClient("MinApi");
        return await client.GetFromJsonAsync<List<Produkt>>("produkter");
    }
}

Intelligent caching af API-svar

Caching er en af de nemmeste sejre du kan få (og en af dem der oftest overses):

public class CachedApiService : IApiService
{
    private readonly IApiService _innerService;
    private readonly IMemoryCache _cache;
    
    public CachedApiService(IApiService innerService, IMemoryCache cache)
    {
        _innerService = innerService;
        _cache = cache;
    }
    
    public async Task<List<Produkt>> HentProdukterAsync()
    {
        const string cacheKey = "produkter_liste";
        
        if (_cache.TryGetValue(cacheKey, out List<Produkt> cachedData))
        {
            return cachedData;
        }
        
        var data = await _innerService.HentProdukterAsync();
        
        var cacheOptions = new MemoryCacheEntryOptions()
            .SetAbsoluteExpiration(TimeSpan.FromMinutes(5))
            .SetSlidingExpiration(TimeSpan.FromMinutes(2));
        
        _cache.Set(cacheKey, data, cacheOptions);
        
        return data;
    }
}

Profileringsværktøjer: Mål før du optimerer

Den vigtigste regel inden for ydeevneoptimering er denne: mål altid før du optimerer. Intuition er notorisk upålidelig når det gælder ydeevne. Det du tror er flaskehalsen? Det er det sjældent.

dotnet-trace til profilering

dotnet-trace er det primære profileringsværktøj for .NET MAUI-apps og virker på alle platforme:

# Installer dotnet-trace
dotnet tool install --global dotnet-trace

# Start profilering af en kørende app
dotnet trace collect --process-id <PID> --providers Microsoft-Diagnostics-DiagnosticSource

# Konverter til speedscope-format for visualisering
dotnet trace convert trace.nettrace --format speedscope

Debug vs. Release: Glem det ikke

En klassisk fejl mange udviklere begår er at evaluere ydeevne i Debug-konfiguration. Debug-builds inkluderer ekstra checks, deaktiverer optimeringer og bruger JIT-kompilering. Test altid ydeevne i Release-konfiguration — forskellen kan ærligt talt være enorm.

# Byg i Release-tilstand
dotnet build -c Release

# Kør på fysisk enhed (ikke emulator) for realistisk ydeevne
dotnet build -c Release -t:Run -f net10.0-android

Platformspecifik profilering

  • Android: Brug Android Profiler i Android Studio til at overvåge CPU, hukommelse og netværk i realtid.
  • iOS: Brug Instruments i Xcode — specielt Time Profiler, Allocations og Leaks-instrumenterne.
  • Windows: Brug PerfView eller Visual Studios indbyggede diagnosticeringsværktøjer.

Platform-specifikke optimeringer

Android: Startup Tracing

Android-apps kan drage fordel af startup tracing, som profilerer de mest brugte kodestier under opstart og forudkompilerer dem. Det er en nem sejr:

<!-- I .csproj for Android -->
<PropertyGroup Condition="$(TargetFramework.Contains('android'))">
    <AndroidEnableProfiledAot>true</AndroidEnableProfiledAot>
</PropertyGroup>

iOS: Reducér appstørrelsen

På iOS er den endelige appstørrelse vigtig for download-hastighed og App Store-begrænsninger. Brug linkeren aggressivt i Release-builds:

<!-- I .csproj for iOS -->
<PropertyGroup Condition="$(TargetFramework.Contains('ios'))">
    <MtouchLink>Full</MtouchLink>
    <EnableSGenConc>true</EnableSGenConc>
</PropertyGroup>

Tjekliste til ydeevneoptimering

Inden du releaser din app, bør du løbe denne tjekliste igennem:

  1. Opstartstid: Er appen klar inden for 2-3 sekunder på en gennemsnitslig enhed?
  2. Compiled bindings: Bruger alle sider x:DataType?
  3. CollectionView: Er virtualiseringen intakt? Er lister IKKE inde i ScrollView?
  4. Hukommelse: Er alle event handlers afregistreret i OnDisappearing?
  5. Layout: Er indlejringsniveauet under 4?
  6. Netværk: Bruges IHttpClientFactory? Er API-svar cachet?
  7. Billeder: Er billeder størrelsesoptimerede? Bruges caching korrekt?
  8. DI: Er services registreret med den korrekte livstid (Singleton vs. Transient)?
  9. Build: Er AOT/trimming aktiveret i Release-konfigurationen?
  10. Test: Er ydeevnen testet i Release-konfiguration på fysiske enheder?

Konklusion: Ydeevne er en rejse, ikke en destination

Ydeevneoptimering er ikke noget du gør én gang og så glemmer alt om. Det er en løbende praksis der bør være en del af hele udviklingscyklussen — fra den første linje kode til den endelige release og videre ind i vedligeholdelsesfasen.

Her er de vigtigste ting at tage med fra denne guide:

  • Mål først, optimer dernæst. Brug profilerings­værktøjer til at finde de reelle flaskehalse. Lad være med at gætte.
  • Compiled bindings er ikke valgfrit. Med Native AOT og fuld trimming er de et krav — og ydeevnegevinsten er helt reel.
  • Respektér virtualiseringen. CollectionView er hurtig, hvis du lader den gøre sit arbejde korrekt.
  • Udskyd alt der kan udskydes. Lazy loading, asynkron initialisering og inkrementel datahentning gør en enorm forskel i praksis.
  • Test på rigtige enheder i Release-konfiguration. Emulatorens ydeevne er ikke repræsentativ — det kan man bare ikke komme udenom.

Med disse teknikker og principper er du godt rustet til at bygge .NET MAUI-applikationer der ikke bare fungerer, men som føles hurtige og responsive. Præcis som brugerne forventer det.

Om Forfatteren Editorial Team

Our team of expert writers and editors.