Optimizarea Performanței în .NET MAUI: De la Startup la Profilare

Ghid practic de optimizare a performanței în .NET MAUI: NativeAOT, Profiled AOT, CollectionView cu reducere de 90% a alocărilor, binding-uri compilate, gestionarea memoriei și profilare cu dotnet-trace și dotnet-gcdump.

Introducere: De Ce Contează Performanța (Mai Mult Decât Crezi)

Hai să fim sinceri: într-o piață în care utilizatorii dezinstalează aplicațiile care nu se încarcă în 3 secunde, performanța nu mai e un lux — e o condiție de supraviețuire. Studiile arată că 53% dintre utilizatori abandonează o aplicație mobilă dacă durează mai mult de 3 secunde să se încarce. Trei secunde. Atât.

Și nu e doar despre percepție. O întârziere de doar 100 de milisecunde în răspunsul interfeței poate reduce rata de conversie cu 7%.

.NET MAUI a evoluat enorm în ultimii ani. Odată cu .NET 10, Microsoft a pus un accent serios pe calitatea și performanța framework-ului. Dar (și e un „dar" important) o aplicație MAUI rapidă nu se construiește automat — necesită alegeri arhitecturale corecte, configurări specifice platformei și atenție constantă la profilare.

În acest ghid, vom trece prin toate aspectele critice ale optimizării: de la timpul de pornire și compilarea AOT, la CollectionView, gestionarea memoriei, binding-uri compilate și tehnici de profilare. Fiecare secțiune include cod pe care îl puteți aplica imediat. Deci, să începem.

Optimizarea Timpului de Pornire al Aplicației

Timpul de pornire e prima impresie. Un startup lent înseamnă recenzii negative și dezinstalări. Vestea bună? Avem mai multe instrumente la dispoziție decât v-ați aștepta.

NativeAOT — Compilarea Ahead-of-Time pentru iOS și macOS

NativeAOT este probabil cea mai impactantă optimizare disponibilă pentru platformele Apple. Spre deosebire de compilarea JIT tradițională, NativeAOT compilă tot codul la momentul build-ului, direct în cod nativ. Rezultatul? Timpi de pornire mult mai mici și o dimensiune redusă a aplicației.

Pentru a activa NativeAOT, adăugați următoarele în fișierul .csproj:

<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">
    <PublishAot>true</PublishAot>
    <PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>

<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">
    <PublishAot>true</PublishAot>
    <PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>

Atenție: NativeAOT limitează utilizarea reflecției. Dacă folosiți biblioteci terțe care depind de reflecție, verificați compatibilitatea înainte de activare. Sincer, singura modalitate sigură este să publicați cu NativeAOT și să vedeți dacă apar avertismente — nu e elegant, dar funcționează.

Profiled AOT pentru Android

Pe Android, NativeAOT nu e încă disponibil, dar avem Profiled AOT (sau Startup Tracing, cum mai e cunoscut). Ideea e simplă: compilează AOT doar metodele apelate la pornire, oferind un echilibru bun între viteză și dimensiune.

<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
    <AndroidEnableProfiledAot>true</AndroidEnableProfiledAot>
    <RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>

Testele interne Microsoft arată o îmbunătățire de aproximativ 25ms pe un Pixel 6 Pro. Nu pare mult, dar pe dispozitive mai lente câștigul e mult mai vizibil.

IL Trimming — Eliminarea Codului Nefolosit

IL Trimming face exact ce sugerează numele: elimină assemblies, metode și tipuri neutilizate din build. Aplicația devine mai mică și are mai puțin cod de inițializat. Combinat cu AOT, rezultatele sunt și mai bune.

<PropertyGroup>
    <PublishTrimmed>true</PublishTrimmed>
    <TrimMode>link</TrimMode>
</PropertyGroup>

Dacă sunteți autor de bibliotecă NuGet, marcați-o ca compatibilă cu trimming-ul și AOT:

<PropertyGroup>
    <IsTrimmable>true</IsTrimmable>
    <IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

ReadyToRun pentru Windows

Pe Windows, unde se folosește runtime-ul CoreCLR, ReadyToRun (R2R) e varianta de compilare AOT care îmbunătățește semnificativ startup-ul. Vestea bună: template-urile WinUI3 activează deja PublishReadyToRun implicit pentru Release, iar .NET MAUI a adoptat acest comportament. Deci n-aveți nimic de configurat manual aici.

Amânarea Logicii Grele la Pornire

Indiferent de optimizările de compilare, una dintre cele mai eficiente strategii rămâne cea mai simplă: amânați încărcarea datelor. Nu încărcați niciodată date în constructorul paginii principale. Serios, niciodată. Folosiți OnAppearing cu apeluri asincrone:

public partial class MainPage : ContentPage
{
    private readonly IDataService _dataService;
    private bool _isDataLoaded = false;

    public MainPage(IDataService dataService)
    {
        InitializeComponent();
        _dataService = dataService;
    }

    protected override async void OnAppearing()
    {
        base.OnAppearing();

        if (!_isDataLoaded)
        {
            // Afișăm indicator de încărcare
            LoadingIndicator.IsVisible = true;
            MainContent.IsVisible = false;

            try
            {
                var data = await _dataService.GetInitialDataAsync();
                BindingContext = new MainViewModel(data);
                _isDataLoaded = true;
            }
            finally
            {
                LoadingIndicator.IsVisible = false;
                MainContent.IsVisible = true;
            }
        }
    }
}

Optimizarea CollectionView pentru Performanță Maximă

CollectionView e una dintre cele mai utilizate componente în aplicațiile mobile — și, din păcate, una dintre cele mai frecvente surse de probleme de performanță. Am văzut cazuri în care 7.000 de obiecte „ucid" literalmente performanța. Dar cu câteva ajustări (surprinzător de simple), puteți reduce alocarea de obiecte cu peste 90%.

Regula de Aur: ItemSizingStrategy

Cel mai important atribut pentru performanța CollectionView este ItemSizingStrategy. Valoarea implicită, MeasureAllItems, face ca framework-ul să măsoare dimensiunea fiecărui element individual — o operație extrem de costisitoare pentru liste mari.

<!-- GREȘIT: măsoară fiecare element individual -->
<CollectionView ItemsSource="{Binding Items}">
    <!-- ... -->
</CollectionView>

<!-- CORECT: măsoară doar primul element -->
<CollectionView ItemsSource="{Binding Items}"
                ItemSizingStrategy="MeasureFirstItem">
    <!-- ... -->
</CollectionView>

Cu MeasureFirstItem, MAUI măsoară doar primul element și reutilizează dimensiunea pentru restul. În teste practice, alocarea a scăzut de la 7.014 la doar 417 obiecte. Da, ați citit bine — o reducere de peste 90%.

Aplatizarea Layout-ului din DataTemplate

StackLayout e costisitor pentru că declanșează cicluri multiple de layout la calcularea dimensiunii copiilor. Într-un CollectionView, costul ăsta se multiplică cu fiecare rând derulat. Soluția: înlocuiți layout-urile imbricate cu un singur Grid plat și folosiți Border în loc de Frame (care e mult mai eficient).

<!-- GREȘIT: layout-uri imbricate -->
<DataTemplate x:DataType="models:Product">
    <Frame Padding="10" Margin="5">
        <StackLayout>
            <StackLayout Orientation="Horizontal">
                <Image Source="{Binding ImageUrl}" />
                <StackLayout>
                    <Label Text="{Binding Name}" />
                    <Label Text="{Binding Price}" />
                </StackLayout>
            </StackLayout>
            <Label Text="{Binding Description}" />
        </StackLayout>
    </Frame>
</DataTemplate>

<!-- CORECT: layout plat cu Grid -->
<DataTemplate x:DataType="models:Product">
    <Border Padding="10" Margin="5"
            StrokeShape="RoundRectangle 8">
        <Grid ColumnDefinitions="60, *"
              RowDefinitions="Auto, Auto, Auto"
              ColumnSpacing="10"
              RowSpacing="4">
            <Image Source="{Binding ImageUrl}"
                   Grid.RowSpan="2"
                   WidthRequest="60"
                   HeightRequest="60"
                   Aspect="AspectFill" />
            <Label Text="{Binding Name}"
                   Grid.Column="1"
                   FontAttributes="Bold" />
            <Label Text="{Binding Price}"
                   Grid.Column="1"
                   Grid.Row="1" />
            <Label Text="{Binding Description}"
                   Grid.ColumnSpan="2"
                   Grid.Row="2" />
        </Grid>
    </Border>
</DataTemplate>

Nu Plasați Niciodată CollectionView într-un ScrollView

Aceasta e o greșeală pe care o văd surprinzător de des. Când puneți un CollectionView într-un ScrollView, virtualizarea se dezactivează complet — toate elementele se randează simultan, indiferent câte sunt vizibile.

În schimb, plasați-l într-un Grid cu o înălțime de rând *:

<Grid RowDefinitions="Auto, *">
    <SearchBar Grid.Row="0"
               Placeholder="Caută produse..." />

    <CollectionView Grid.Row="1"
                    ItemsSource="{Binding Products}"
                    ItemSizingStrategy="MeasureFirstItem">
        <!-- DataTemplate aici -->
    </CollectionView>
</Grid>

Simplu, dar face o diferență enormă.

Încărcare Incrementală (Infinite Scrolling)

Pentru seturi mari de date, nu încărcați tot dintr-o dată. CollectionView suportă nativ încărcarea incrementală prin RemainingItemsThreshold:

public class ProductsViewModel : ObservableObject
{
    private readonly IProductService _productService;
    private int _currentPage = 0;
    private const int PageSize = 20;

    public ObservableCollection<Product> Products { get; } = new();

    [ObservableProperty]
    private bool _isLoadingMore;

    public IAsyncRelayCommand LoadMoreCommand { get; }

    public ProductsViewModel(IProductService productService)
    {
        _productService = productService;
        LoadMoreCommand = new AsyncRelayCommand(LoadMoreAsync);
    }

    private async Task LoadMoreAsync()
    {
        if (IsLoadingMore) return;

        try
        {
            IsLoadingMore = true;
            _currentPage++;
            var newItems = await _productService
                .GetProductsAsync(_currentPage, PageSize);

            foreach (var item in newItems)
            {
                Products.Add(item);
            }
        }
        finally
        {
            IsLoadingMore = false;
        }
    }
}
<CollectionView ItemsSource="{Binding Products}"
                ItemSizingStrategy="MeasureFirstItem"
                RemainingItemsThreshold="5"
                RemainingItemsThresholdReachedCommand="{Binding LoadMoreCommand}">
    <!-- DataTemplate -->
</CollectionView>

Binding-uri Compilate și Compilarea XAML

Binding-urile implicite din .NET MAUI folosesc reflecția pentru a accesa proprietățile obiectelor. E un proces costisitor, mai ales în pagini cu multe binding-uri. Din fericire, .NET MAUI oferă un mecanism care rezolvă binding-urile la momentul compilării.

Activarea Binding-urilor Compilate

Tot ce trebuie să faceți e să setați atributul x:DataType la tipul obiectului din BindingContext:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:viewmodels="clr-namespace:MyApp.ViewModels"
             x:Class="MyApp.Views.ProductDetailPage"
             x:DataType="viewmodels:ProductDetailViewModel">

    <VerticalStackLayout Padding="16">
        <!-- Aceste binding-uri sunt compilate -->
        <Label Text="{Binding ProductName}"
               FontSize="24"
               FontAttributes="Bold" />
        <Label Text="{Binding Price, StringFormat='Preț: {0:C}'}" />
        <Label Text="{Binding Description}" />
        <Button Text="Adaugă în coș"
                Command="{Binding AddToCartCommand}" />
    </VerticalStackLayout>
</ContentPage>

Binding-urile compilate elimină overhead-ul reflecției și generează cod strongly-typed. În .NET 10, compilarea XAML a fost îmbunătățită semnificativ — generatorul de surse XAML creează cod strongly-typed pentru fișierele XAML, reducând și mai mult overhead-ul la runtime.

Generatorul de Surse XAML în .NET 10

.NET MAUI 10 vine cu un generator de surse pentru XAML care îmbunătățește performanța build-ului. Pentru a-l activa:

<PropertyGroup>
    <MauiXamlInflator>SourceGen</MauiXamlInflator>
</PropertyGroup>

Acest generator creează cod la compilare pentru metodele InitializeComponent(), eliminând interpretarea XAML la runtime. Performanța de inflație a view-urilor e semnificativ mai rapidă, mai ales în modul debug (unde diferența se simte cel mai tare).

Gestionarea Eficientă a Memoriei

Scurgerile de memorie sunt una dintre cele mai insidioase probleme din aplicațiile mobile. Spre deosebire de un ecran care se încarcă lent (pe care-l observi imediat), scurgerile se acumulează în timp. Aplicația merge bine la început, apoi încetinește progresiv până când — crash.

Surse Comune de Scurgeri de Memorie în .NET MAUI

Din experiența mea, cele mai frecvente surse sunt:

  • Abonamente la evenimente nedesubscrise: Când un obiect se abonează la un eveniment al altui obiect cu durată de viață mai lungă, referința previne garbage collection-ul. E clasicul memory leak.
  • Handler-e nedispose: .NET MAUI folosește un sistem de handler-e care conectează controalele cross-platform la implementările native. Dacă nu le eliberați corect, obiectele native rămân în memorie.
  • Referințe circulare între C# și platforma nativă: Pe Android, asta se manifestă prin acumularea de GREFs (Global References). Peste 46.000 de GREFs? Aveți o scurgere.
  • Pagini care nu se dezalocă la navigarea înapoi: Am întâlnit cazuri în care paginile și ViewModel-urile tranziente nu se curățau corect după navigarea back.

Implementarea Modelului Dispose Corect

public class ProductDetailPage : ContentPage
{
    private readonly IMessenger _messenger;
    private CancellationTokenSource? _cts;

    public ProductDetailPage(
        ProductDetailViewModel viewModel,
        IMessenger messenger)
    {
        InitializeComponent();
        BindingContext = viewModel;
        _messenger = messenger;

        // Abonare la mesaje
        _messenger.Register<ProductUpdatedMessage>(
            this, (r, m) => OnProductUpdated(m));
    }

    protected override void OnAppearing()
    {
        base.OnAppearing();
        _cts = new CancellationTokenSource();
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();

        // Anulăm operațiile în curs
        _cts?.Cancel();
        _cts?.Dispose();
        _cts = null;

        // Desubscriere de la mesaje
        _messenger.UnregisterAll(this);
    }

    private void OnProductUpdated(ProductUpdatedMessage message)
    {
        // Actualizare UI
    }
}

Utilizarea WeakEventManager pentru Evenimente

Când expuneți evenimente personalizate, folosiți WeakEventManager pentru a preveni scurgerile cauzate de abonamentele la evenimente:

public class DataSyncService
{
    private readonly WeakEventManager _eventManager = new();

    public event EventHandler<SyncCompletedEventArgs> SyncCompleted
    {
        add => _eventManager.AddEventHandler(value);
        remove => _eventManager.RemoveEventHandler(value);
    }

    public async Task SyncAsync()
    {
        // Logică de sincronizare...
        _eventManager.HandleEvent(
            this,
            new SyncCompletedEventArgs(success: true),
            nameof(SyncCompleted));
    }
}

Profilarea Performanței cu Instrumente .NET

Nu poți optimiza ceea ce nu poți măsura. E un clișeu, dar e adevărat. .NET oferă un set solid de instrumente de diagnosticare care funcționează cu aplicațiile MAUI pe toate platformele.

Instalarea Instrumentelor de Diagnosticare

Aveți nevoie de trei instrumente globale:

dotnet tool install -g dotnet-trace
dotnet tool install -g dotnet-gcdump
dotnet tool install -g dotnet-dsrouter

Important: Aveți nevoie de cel puțin versiunea 9.0.652701. Începând cu această versiune, atât dotnet-trace cât și dotnet-gcdump includ opțiunea --dsrouter care lansează și gestionează automat dotnet-dsrouter ca subproces. Asta simplifică mult fluxul de lucru.

Profilarea CPU cu dotnet-trace

Pentru a identifica bottleneck-urile, dotnet-trace colectează informații despre execuția CPU — ce metode consumă cel mai mult timp și unde ar trebui să vă concentrați eforturile.

# Pentru aplicații Android (cu dsrouter)
dotnet-trace collect --dsrouter android \
    --providers Microsoft-DotNETRuntimeMonoProfiler:0xC900003:4 \
    --output maui-trace.nettrace

# Pentru aplicații iOS (cu dsrouter)
dotnet-trace collect --dsrouter ios \
    --providers Microsoft-DotNETRuntimeMonoProfiler:0xC900003:4 \
    --output maui-trace.nettrace

Fișierele .nettrace pot fi analizate în Visual Studio sau convertite în format speedscope pentru vizualizare direct în browser.

Capturarea Snapshot-urilor de Memorie cu dotnet-gcdump

Pentru scurgeri de memorie, dotnet-gcdump e instrumentul potrivit. Un avantaj mare: snapshot-urile de memorie nu necesită suspendarea pornirii aplicației:

# Construiți aplicația cu DiagnosticSuspend dezactivat
dotnet build -p:DiagnosticSuspend=false

# Capturați snapshot-ul de memorie
dotnet-gcdump collect --dsrouter android \
    --output memory-snapshot.gcdump

Puteți lua multiple snapshot-uri în timp ce aplicația rulează. Comparați-le înainte și după un scenariu specific pentru a vedea ce obiecte se acumulează.

Un detaliu care m-a prins: dezactivați XAML Hot Reload când luați snapshot-uri de memorie. Altfel, rezultatele nu vor reflecta realitatea.

Profilarea cu .NET Meteor în VS Code

Dacă preferați VS Code, extensia .NET Meteor (versiunea 4.0+) integrează instrumentele de diagnosticare direct în editor. Oferă două moduri: Trace pentru bottleneck-uri de performanță și GCDump pentru scurgeri de memorie. E o alternativă excelentă pentru cei care nu lucrează în Visual Studio.

Arhitectura Aplicației pentru Performanță

Optimizarea nu se rezumă la trucuri punctuale. Arhitectura aplicației joacă un rol fundamental — alegerile de structură se reflectă direct în performanța generală.

Dependency Injection Eficient

.NET MAUI folosește Microsoft.Extensions.DependencyInjection pentru gestionarea dependențelor. Modul în care înregistrați serviciile chiar contează:

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });

        // Servicii singleton — o singură instanță pe durata aplicației
        builder.Services.AddSingleton<ISettingsService, SettingsService>();
        builder.Services.AddSingleton<IAuthService, AuthService>();
        builder.Services.AddSingleton<HttpClient>(sp =>
        {
            var client = new HttpClient
            {
                BaseAddress = new Uri("https://api.example.com/"),
                Timeout = TimeSpan.FromSeconds(30)
            };
            return client;
        });

        // Servicii tranziente — instanță nouă la fiecare rezolvare
        builder.Services.AddTransient<MainPage>();
        builder.Services.AddTransient<MainViewModel>();
        builder.Services.AddTransient<ProductDetailPage>();
        builder.Services.AddTransient<ProductDetailViewModel>();

        // Servicii scoped — rar utilizate în MAUI,
        // dar utile pentru operații cu durată limitată
        builder.Services.AddScoped<IOrderService, OrderService>();

        return builder.Build();
    }
}

Pe scurt:

  • AddSingleton pentru servicii care nu se schimbă: HttpClient, configurare, cache
  • AddTransient pentru pagini și ViewModel-uri — crearea instanțelor e ieftină și previne problemele de stare
  • Înregistrați totul într-un singur loc (MauiProgram) pentru claritate

CommunityToolkit.MVVM pentru Performanță Optimă

CommunityToolkit.MVVM rămâne framework-ul MVVM recomandat pentru .NET MAUI. Folosește source generators care elimină overhead-ul la runtime și generează codul necesar la compilare:

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

public partial class ProductListViewModel : ObservableObject
{
    private readonly IProductService _productService;

    // Source generator creează proprietatea ProductName
    // cu notificări INotifyPropertyChanged
    [ObservableProperty]
    private string _searchQuery = string.Empty;

    [ObservableProperty]
    private bool _isLoading;

    // Proprietatea calculată se actualizează automat
    // când SearchQuery se schimbă
    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(HasSearchQuery))]
    private string _filterText = string.Empty;

    public bool HasSearchQuery => !string.IsNullOrEmpty(FilterText);

    public ObservableCollection<Product> Products { get; } = new();

    public ProductListViewModel(IProductService productService)
    {
        _productService = productService;
    }

    [RelayCommand]
    private async Task SearchAsync()
    {
        if (IsLoading) return;

        try
        {
            IsLoading = true;
            var results = await _productService
                .SearchAsync(SearchQuery);

            Products.Clear();
            foreach (var product in results)
            {
                Products.Add(product);
            }
        }
        finally
        {
            IsLoading = false;
        }
    }
}

Marele avantaj: zero reflecție la runtime. Tot codul de notificare e generat la compilare, ceea ce face ViewModel-urile la fel de performante ca și codul scris manual.

Anti-Pattern-uri de Evitat

Câteva greșeli frecvente care afectează performanța:

  • „God ViewModel": Un singur ViewModel care face totul — date, navigare, business logic, stare UI, validare. Soluția e simplă: responsabilitate unică, delegați logica către servicii.
  • Cod în code-behind: Logica plasată în code-behind face testarea dificilă. Mutați-o în ViewModel-uri sau servicii.
  • Stare statică: Variabilele statice pentru starea aplicației introduc bug-uri greu de depanat. Evitați-le.
  • Apeluri sincrone în constructori: Nu executați operații I/O sau apeluri de rețea în constructori. Punct. Folosiți metode async din OnAppearing sau comenzi.

Optimizarea Imaginilor și a Resurselor

Imaginile sunt de obicei cel mai mare consumator de memorie într-o aplicație mobilă. Iată câteva strategii care chiar fac diferența.

Dimensionarea Corectă a Imaginilor

Nu încărcați niciodată imagini la rezoluția completă dacă le afișați la o dimensiune mai mică. Folosiți proprietățile de dimensionare și cache-ul:

<Image Source="{Binding ThumbnailUrl}"
       WidthRequest="80"
       HeightRequest="80"
       Aspect="AspectFill">
    <Image.Source>
        <UriImageSource Uri="{Binding ThumbnailUrl}"
                        CacheValidity="7:00:00:00"
                        CachingEnabled="True" />
    </Image.Source>
</Image>

Cache-ul de Imagini și Încărcarea Asincronă

Pentru scenarii mai complexe, o bibliotecă dedicată precum FFImageLoading sau Comet oferă:

  • Cache pe disc și în memorie cu politici de expirare configurabile
  • Downsampling automat bazat pe dimensiunea de afișare
  • Placeholder-uri și animații de tranziție
  • Anularea automată a descărcărilor pentru imaginile care nu mai sunt vizibile

Configurația Release și Testarea Performanței

O greșeală pe care o văd surprinzător de des: evaluarea performanței în modul Debug. Diferența dintre Debug și Release e dramatică în .NET MAUI:

  • Debug: JIT complet, fără optimizări, XAML Hot Reload activ, diagnostice adiționale
  • Release: AOT compilat, optimizări activate, trimming, dimensiune redusă

Testați întotdeauna performanța în Release, pe dispozitive fizice. Emulatoarele rulează pe hardware-ul gazdei și pot masca probleme reale. Am pierdut ore întregi depanând „probleme de performanță" care existau doar pe emulatoare.

Benchmark-uri și Metrici de Urmărit

Stabiliți benchmark-uri și monitorizați-le regulat:

  • Cold Start: De la atingerea iconiței până când pagina e interactivă. Ținta: sub 2 secunde pe dispozitive mid-range.
  • Warm Start: Reactivarea aplicației. Ținta: sub 500ms.
  • Frame rate la derulare: CollectionView ar trebui să mențină constant 60fps.
  • Utilizarea memoriei: O creștere constantă în timp = scurgere de memorie.
  • Dimensiunea aplicației: Cu optimizările corecte, aplicațiile iOS ar trebui să fie sub 20MB.

Integrarea .NET Aspire pentru Telemetrie

.NET MAUI 10 introduce un template de proiect care creează un proiect de service defaults .NET Aspire. Oferă extensii care conectează telemetria și descoperirea serviciilor, permițând monitorizarea performanței în producție:

// În MauiProgram.cs
builder.Services.AddServiceDefaults();

// Aceasta configurează automat:
// - OpenTelemetry pentru trace-uri distribuite
// - Metrici de performanță
// - Logging structurat
// - Service discovery

Cu Aspire integrat, puteți monitoriza performanța în producție și identifica probleme înainte ca utilizatorii să le raporteze. E un game-changer pentru aplicațiile în producție.

Optimizări Specifice Platformei

Deși .NET MAUI oferă o abstracție cross-platform, fiecare platformă are particularitățile ei. Haideți să le luăm pe rând.

Android: Reducerea Dimensiunii APK-ului

Pe Android, dimensiunea contează — literal. Aplicațiile mai mari de 150MB necesită Wi-Fi pentru descărcare, iar fiecare 6MB suplimentari reduc rata de instalare cu 1%. Pentru optimizare:

<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
    <!-- Activează linking-ul pentru a elimina codul neutilizat -->
    <AndroidLinkMode>SdkOnly</AndroidLinkMode>

    <!-- Compresie optimizată pentru resurse -->
    <AndroidUseAapt2>true</AndroidUseAapt2>

    <!-- Generează un App Bundle în loc de APK -->
    <AndroidPackageFormat>aab</AndroidPackageFormat>

    <!-- Activează R8/ProGuard pentru shrinking suplimentar -->
    <AndroidEnableR8>true</AndroidEnableR8>
</PropertyGroup>

App Bundle-urile permit Google Play să genereze APK-uri optimizate per dispozitiv, incluzând doar resursele necesare. Reducerea poate fi de 20-30% față de un APK universal.

iOS: Optimizări pentru App Store

Pe iOS, pe lângă NativeAOT, activați optimizări suplimentare pentru producție:

<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">
    <!-- Optimizare agresivă a linker-ului -->
    <MtouchLink>SdkOnly</MtouchLink>

    <!-- Activează LLVM pentru optimizări suplimentare -->
    <MtouchUseLlvm>true</MtouchUseLlvm>

    <!-- Strip simbolurile de debug -->
    <MtouchDebug>false</MtouchDebug>
</PropertyGroup>

Cu .NET 10, aplicațiile iOS .NET MAUI au ajuns la aproximativ 15-20MB — o scădere semnificativă față de cele 30-40MB de la lansarea framework-ului.

Gestionarea Eficientă a Colecțiilor de Date

Modul în care gestionați colecțiile afectează direct performanța UI. ObservableCollection e bun pentru modificări individuale, dar devine ineficient la operații batch. Fiecare Add declanșează o notificare separată și un ciclu de layout:

// INEFICIENT: Adăugarea individuală declanșează
// CollectionChanged pentru fiecare element
foreach (var item in newItems)
{
    Products.Add(item); // N notificări separate!
}

// EFICIENT: Utilizați RangeObservableCollection
// pentru operații batch
public class RangeObservableCollection<T> : ObservableCollection<T>
{
    private bool _suppressNotification = false;

    public void AddRange(IEnumerable<T> items)
    {
        _suppressNotification = true;
        foreach (var item in items)
        {
            Items.Add(item);
        }
        _suppressNotification = false;
        OnCollectionChanged(
            new NotifyCollectionChangedEventArgs(
                NotifyCollectionChangedAction.Reset));
    }

    public void ReplaceAll(IEnumerable<T> items)
    {
        _suppressNotification = true;
        Items.Clear();
        foreach (var item in items)
        {
            Items.Add(item);
        }
        _suppressNotification = false;
        OnCollectionChanged(
            new NotifyCollectionChangedEventArgs(
                NotifyCollectionChangedAction.Reset));
    }

    protected override void OnCollectionChanged(
        NotifyCollectionChangedEventArgs e)
    {
        if (!_suppressNotification)
            base.OnCollectionChanged(e);
    }
}

Diferența e semnificativă: în loc de N notificări care forțează N re-randări, colecția batch trimite o singură notificare. Pentru 500 de elemente, asta înseamnă 499 de cicluri de layout eliminate. Nu-i puțin lucru.

Optimizarea Navigării între Pagini

Navigarea poate introduce întârzieri perceptibile dacă nu-i acordați atenție. Câteva tehnici utile:

// Înregistrați rutele pentru navigare eficientă
public static class AppRoutes
{
    public static void RegisterRoutes()
    {
        Routing.RegisterRoute("productDetail",
            typeof(ProductDetailPage));
        Routing.RegisterRoute("checkout",
            typeof(CheckoutPage));
        Routing.RegisterRoute("orderHistory",
            typeof(OrderHistoryPage));
    }
}

// Navigare cu parametri — evitați transmiterea
// obiectelor mari prin querystring
public partial class ProductListViewModel : ObservableObject
{
    [RelayCommand]
    private async Task NavigateToDetailAsync(Product product)
    {
        // Transmiteți doar ID-ul, nu obiectul complet
        await Shell.Current.GoToAsync(
            $"productDetail?id={product.Id}");
    }
}

// În pagina de destinație, încărcați datele pe baza ID-ului
[QueryProperty(nameof(ProductId), "id")]
public partial class ProductDetailViewModel : ObservableObject
{
    [ObservableProperty]
    private string _productId = string.Empty;

    partial void OnProductIdChanged(string value)
    {
        // Încărcăm datele asincron după primirea ID-ului
        LoadProductCommand.Execute(null);
    }

    [RelayCommand]
    private async Task LoadProductAsync()
    {
        var product = await _productService
            .GetByIdAsync(ProductId);
        // Actualizăm proprietățile...
    }
}

Transmiterea doar a ID-urilor în loc de obiecte complete reduce overhead-ul de serializare. Iar încărcarea asincronă permite afișarea imediată a paginii cu un indicator de încărcare — utilizatorul primește feedback vizual instantaneu.

Concluzii și Checklist de Optimizare

Optimizarea performanței în .NET MAUI e un proces continuu, nu ceva ce faci o dată și uiți. Iată un checklist pe care merită să-l aveți la îndemână:

  1. Compilare: Activați NativeAOT (iOS/macOS), Profiled AOT (Android), ReadyToRun (Windows) și IL Trimming
  2. Startup: Amânați încărcarea datelor după afișarea UI-ului, folosiți async/await corect
  3. CollectionView: Setați ItemSizingStrategy="MeasureFirstItem", aplatizați layout-urile, nu imbricați în ScrollView
  4. Binding-uri: Folosiți x:DataType pentru binding-uri compilate, activați generatorul de surse XAML
  5. Memorie: Desubscrieți evenimentele, folosiți WeakEventManager, implementați Dispose corect
  6. Imagini: Dimensionați corect, activați cache-ul, folosiți downsampling
  7. Profilare: Măsurați regulat cu dotnet-trace și dotnet-gcdump, testați pe build-uri Release
  8. Arhitectură: CommunityToolkit.MVVM cu source generators, DI configurat corect

Performanța bună nu vine din magie — vine din decizii informate și măsurători constante. Nu încercați să optimizați totul dintr-o dată. Începeți cu profilarea, identificați bottleneck-urile reale, și lucrați de acolo. De cele mai multe ori, 80% din câștig vine din optimizarea a 20% din cod.

Prioritizați pe baza datelor, nu pe baza presupunerilor. Măsurați înainte și după fiecare schimbare. Și nu uitați: cu fiecare versiune nouă de .NET, Microsoft aduce îmbunătățiri semnificative. În .NET 10, handler-ele CollectionView și CarouselView au fost optimizate pe toate platformele, generatorul de surse XAML oferă performanță mai bună la build și runtime, iar integrarea cu .NET Aspire deschide posibilități noi de monitorizare. Framework-ul devine din ce în ce mai bun — merită să rămâneți la curent cu evoluțiile.

Despre Autor Editorial Team

Our team of expert writers and editors.