Optimalizace výkonu v .NET MAUI: Kompletní průvodce od startupu po rendering

Praktický průvodce optimalizací výkonu .NET MAUI aplikací pokrývající startup, kompilované bindingy, NativeAOT, správu paměti, CollectionView a profilování. S reálnými příklady kódu pro okamžité použití.

Ú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:DataType na každé úrovni, kde se mění typ kontextu — typicky na stránce a v DataTemplate.
  • 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:DataType pro kompilované bindingy na všech stránkách
  • Nastavte x:DataType v každém DataTemplate
  • Preferujte OneTime binding mode tam, kde není potřeba aktualizace

UI a rendering

  • Používejte Grid místo vnořených StackLayout
  • Nastavte ItemSizingStrategy="MeasureFirstItem" na CollectionView
  • Obalte obsah DataTemplate do Border pro 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 WeakEventManager pro 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.

O Autorovi Editorial Team

Our team of expert writers and editors.