Βελτιστοποίηση Απόδοσης .NET MAUI: Compiled Bindings, NativeAOT και Πρακτικές Τεχνικές

Πρακτικός οδηγός για βελτιστοποίηση απόδοσης σε .NET MAUI. Καλύπτει compiled bindings (8x-20x ταχύτερα), NativeAOT deployment, CollectionView, layouts, διαχείριση μνήμης, async patterns και profiling — με παραδείγματα κώδικα.

Εισαγωγή

Ας πούμε τα πράγματα με το όνομά τους: η απόδοση σε μια εφαρμογή κινητών δεν είναι απλά "nice to have" — είναι κρίσιμη. Οι χρήστες θέλουν άμεση ανταπόκριση, ομαλό scrolling και να μη νιώθουν ότι η μπαταρία τους εξαφανίζεται. Στο .NET MAUI, η Microsoft το έχει πάρει σοβαρά αυτό, ιδιαίτερα με τα .NET 9 και .NET 10 που φέρνουν πραγματικά εντυπωσιακές βελτιώσεις σε χρόνους εκκίνησης, κατανάλωση μνήμης και ταχύτητα UI.

Για όσους δεν είστε εξοικειωμένοι, το .NET MAUI (Multi-platform App UI) είναι το cross-platform framework της Microsoft για ανάπτυξη εφαρμογών σε Android, iOS, macOS και Windows από κοινή βάση κώδικα. Ακούγεται τέλειο, σωστά; Υπάρχει όμως ένα "αλλά": χωρίς σωστή βελτιστοποίηση, η cross-platform φύση του μπορεί να δημιουργήσει προβλήματα — αργή εκκίνηση, lags στο scrolling, φουσκωμένη μνήμη και μεγάλα μεγέθη εγκατάστασης.

Σε αυτόν τον οδηγό θα καλύψουμε τα πάντα: από compiled bindings και NativeAOT deployment, μέχρι διαχείριση μνήμης, async patterns και profiling. Κάθε ενότητα έχει πρακτικά παραδείγματα κώδικα που μπορείτε να χρησιμοποιήσετε αμέσως. Πάμε λοιπόν.

Compiled Bindings και x:DataType

Αν μπορούσα να δώσω μόνο μία συμβουλή βελτιστοποίησης σε κάποιον .NET MAUI developer, θα ήταν αυτή: χρησιμοποιήστε compiled bindings παντού. Τα παραδοσιακά bindings βασίζονται σε reflection κατά τη διάρκεια εκτέλεσης για να αναλύσουν τα binding paths — και αυτό κοστίζει. Τα compiled bindings, αντίθετα, μεταγλωττίζονται κατά τη φάση του build, εξαλείφοντας εντελώς αυτό το overhead.

Πώς Λειτουργούν τα Compiled Bindings

Η ιδέα είναι απλή. Προσθέτετε το attribute x:DataType στο XAML, και ο compiler δημιουργεί κώδικα binding κατά τη μεταγλώττιση αντί να βασίζεται σε reflection. Τα αποτελέσματα; Εντυπωσιακά.

Τα OneWay compiled bindings είναι περίπου 8 φορές ταχύτερα από τα κλασικά, ενώ τα OneTime compiled bindings φτάνουν ακόμα και τις 20 φορές. Ναι, διαβάσατε σωστά.

Παράδειγμα Χωρίς Compiled Bindings (Αργό)

Ένα τυπικό binding χωρίς compile-time validation — η ανάλυση γίνεται μέσω reflection κατά τη διάρκεια εκτέλεσης:

<!-- Without compiled bindings - uses reflection at runtime -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
    <StackLayout>
        <Label Text="{Binding UserName}" />
        <Label Text="{Binding Email}" />
        <Label Text="{Binding PhoneNumber}" />
    </StackLayout>
</ContentPage>

Παράδειγμα Με Compiled Bindings (Γρήγορο)

Με την προσθήκη του x:DataType, τα bindings μεταγλωττίζονται και ο compiler επαληθεύει τα paths κατά το build:

<!-- With compiled bindings - resolved at compile time -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:models="clr-namespace:MyApp.Models"
             x:DataType="models:UserProfile">
    <StackLayout>
        <Label Text="{Binding UserName}" />
        <Label Text="{Binding Email}" />
        <Label Text="{Binding PhoneNumber}" />
    </StackLayout>
</ContentPage>

Η μόνη διαφορά; Τρεις γραμμές κώδικα. Η βελτίωση; Τεράστια.

Compiled Bindings σε DataTemplates

Εδώ γίνεται ακόμα πιο σημαντικό. Τα DataTemplates αναπαράγονται πολλές φορές (μία για κάθε στοιχείο σε μια λίστα), οπότε η εξοικονόμηση πολλαπλασιάζεται:

<CollectionView ItemsSource="{Binding Products}">
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="models:Product">
            <Grid Padding="10" ColumnDefinitions="80,*">
                <Image Source="{Binding ImageUrl}"
                       WidthRequest="60"
                       HeightRequest="60" />
                <StackLayout Grid.Column="1" Spacing="4">
                    <Label Text="{Binding Name}"
                           FontAttributes="Bold"
                           FontSize="16" />
                    <Label Text="{Binding Price, StringFormat='{0:C}'}"
                           TextColor="Green" />
                </StackLayout>
            </Grid>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

Απενεργοποίηση Compiled Bindings Σε Συγκεκριμένα Σημεία

Σε κάποιες περιπτώσεις (π.χ. dynamic data), μπορεί να χρειαστεί να απενεργοποιήσετε τα compiled bindings. Χρησιμοποιήστε x:DataType="" με κενό string:

<StackLayout x:DataType="">
    <!-- These bindings will use reflection (classic mode) -->
    <Label Text="{Binding DynamicProperty}" />
</StackLayout>

Στο .NET 10, η Microsoft έχει βελτιώσει τον compiler για compiled bindings, υποστηρίζοντας περισσότερα σενάρια και δίνοντας καλύτερα μηνύματα σφαλμάτων. Η δική μου σύσταση; Ενεργοποιήστε τα παντού — δεν υπάρχει λόγος να μην το κάνετε.

NativeAOT Deployment

Εδώ τα πράγματα γίνονται ενδιαφέροντα. Η τεχνολογία NativeAOT (Native Ahead-of-Time compilation) αλλάζει ριζικά τον τρόπο που τρέχουν οι .NET εφαρμογές σε κινητά. Αντί για JIT compilation κατά τη διάρκεια εκτέλεσης, ο κώδικας μεταγλωττίζεται απευθείας σε native machine code κατά το build.

Πλεονεκτήματα του NativeAOT

  • Ταχύτερη εκκίνηση: Έως και 2x ταχύτερη εκκίνηση — δεν απαιτείται JIT compilation κατά τη φόρτωση
  • Μικρότερο μέγεθος: Μέσω tree-shaking αφαιρείται κώδικας που δεν χρησιμοποιείται πραγματικά
  • Λιγότερη μνήμη: Η απουσία του JIT compiler μειώνει σημαντικά τη χρήση RAM
  • Προβλέψιμη απόδοση: Τέλος στις τυχαίες παύσεις λόγω JIT compilation

Ενεργοποίηση NativeAOT

Το NativeAOT είναι διαθέσιμο για iOS και Mac Catalyst από το .NET 9, με σημαντικές βελτιώσεις στο .NET 10. Η ρύθμιση στο .csproj είναι εξαιρετικά απλή:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
    <OutputType>Exe</OutputType>
    <UseMaui>true</UseMaui>

    <!-- Enable NativeAOT for iOS and Mac Catalyst -->
    <PublishAot>true</PublishAot>

    <!-- Optional: strip symbols for smaller binaries -->
    <StripSymbols>true</StripSymbols>
  </PropertyGroup>
</Project>

Εκτιμήσεις Συμβατότητας

Βέβαια, δεν είναι όλα τέλεια (πότε είναι;). Το NativeAOT έχει ορισμένους περιορισμούς. Κώδικας που βασίζεται σε reflection μπορεί να χρειαστεί ρυθμίσεις trimming, και βιβλιοθήκες τρίτων πρέπει να είναι συμβατές. Χρησιμοποιήστε τα κατάλληλα attributes:

// Preserve types that are accessed via reflection
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public class MyViewModel
{
    // Properties used in XAML bindings
    public string Title { get; set; }
    public string Description { get; set; }
}

// Use source generators instead of reflection-based serialization
[JsonSerializable(typeof(ApiResponse))]
[JsonSerializable(typeof(List<Product>))]
public partial class AppJsonContext : JsonSerializerContext
{
}

Τα καλά νέα; Στο .NET 10, η συμβατότητα NativeAOT με MAUI controls και handlers έχει βελτιωθεί δραματικά. Τα compatibility issues του .NET 9 ανήκουν σε μεγάλο βαθμό στο παρελθόν, κάνοντας το NativeAOT μια πολύ πιο ρεαλιστική επιλογή για production apps.

Βελτιστοποίηση CollectionView

Το CollectionView είναι ο βασιλιάς των λιστών στο .NET MAUI. Αλλά ένα κακά ρυθμισμένο CollectionView μπορεί να μετατρέψει μια εφαρμογή σε εφιάλτη — ιδιαίτερα σε λίστες με πολλά στοιχεία.

Μην Τοποθετείτε CollectionView Μέσα σε ScrollView

Αυτό είναι ίσως το πιο κλασικό λάθος που βλέπω ξανά και ξανά. Βάζετε ένα CollectionView μέσα σε ScrollView και — μπαμ — ο μηχανισμός virtualization ακυρώνεται εντελώς. Το CollectionView αναγκάζεται να δημιουργήσει views για όλα τα στοιχεία ταυτόχρονα:

<!-- BAD: Breaks virtualization - DO NOT do this -->
<ScrollView>
    <StackLayout>
        <Label Text="Header" FontSize="24" />
        <CollectionView ItemsSource="{Binding Items}">
            <!-- All items rendered at once! -->
        </CollectionView>
    </StackLayout>
</ScrollView>

Σωστή Χρήση με Header και Grid

Η λύση; Χρησιμοποιήστε το CollectionView Header ή Grid με κατάλληλες row definitions:

<!-- GOOD: Using CollectionView Header -->
<CollectionView ItemsSource="{Binding Items}">
    <CollectionView.Header>
        <Label Text="Header" FontSize="24" Padding="10" />
    </CollectionView.Header>
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="models:Item">
            <Grid HeightRequest="60" Padding="10"
                  ColumnDefinitions="50,*,Auto">
                <Image Source="{Binding Icon}"
                       WidthRequest="40"
                       HeightRequest="40" />
                <Label Grid.Column="1"
                       Text="{Binding Name}"
                       VerticalOptions="Center" />
                <Label Grid.Column="2"
                       Text="{Binding Status}"
                       VerticalOptions="Center" />
            </Grid>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

Καθορίστε Σταθερό Ύψος Στοιχείων

Ο μηχανισμός virtualization δουλεύει πολύ καλύτερα όταν ξέρει εκ των προτέρων πόσο ψηλό θα είναι κάθε στοιχείο. Ορίστε ItemSizingStrategy μαζί με σταθερό HeightRequest:

<CollectionView ItemsSource="{Binding Items}"
                ItemSizingStrategy="MeasureFirstItem">
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="models:Item">
            <Grid HeightRequest="72"
                  Padding="16,8"
                  ColumnDefinitions="48,*">
                <Image Source="{Binding Thumbnail}"
                       WidthRequest="48"
                       HeightRequest="48"
                       Aspect="AspectFill" />
                <StackLayout Grid.Column="1"
                             VerticalOptions="Center"
                             Spacing="2">
                    <Label Text="{Binding Title}"
                           FontSize="14"
                           LineBreakMode="TailTruncation" />
                    <Label Text="{Binding Subtitle}"
                           FontSize="12"
                           TextColor="Gray"
                           LineBreakMode="TailTruncation" />
                </StackLayout>
            </Grid>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

Χρήση CachingStrategy

Για βέλτιστη απόδοση, βεβαιωθείτε ότι χρησιμοποιείτε compiled bindings σε όλα τα DataTemplates και αποφεύγετε πολύπλοκα layouts μέσα σε templates. Κάθε επιπλέον επίπεδο nesting πολλαπλασιάζει τον χρόνο rendering — κάτι που γίνεται αισθητό γρήγορα σε λίστες με εκατοντάδες στοιχεία.

Βελτιστοποίηση Layouts

Η δομή των layouts μπορεί να κάνει τεράστια διαφορά στην απόδοση. Κάθε layout container υπολογίζει θέσεις και μεγέθη για τα παιδιά του (layout pass), και η υπερβολική εμφώλευση δημιουργεί αλυσιδωτές αντιδράσεις measure/arrange που επιβραδύνουν σημαντικά το rendering.

Ειλικρινά, αυτό είναι από τα πιο εύκολα πράγματα που μπορείτε να διορθώσετε — και η βελτίωση είναι άμεσα ορατή.

Κακή Πρακτική: Εμφωλευμένα StackLayouts

<!-- BAD: Deeply nested StackLayouts cause excessive layout passes -->
<StackLayout>
    <StackLayout Orientation="Horizontal">
        <StackLayout>
            <Label Text="{Binding Name}" />
            <Label Text="{Binding Title}" />
        </StackLayout>
        <StackLayout HorizontalOptions="End">
            <Label Text="{Binding Date}" />
            <Label Text="{Binding Status}" />
        </StackLayout>
    </StackLayout>
    <StackLayout Orientation="Horizontal">
        <Image Source="{Binding Avatar}" />
        <StackLayout>
            <Label Text="{Binding Description}" />
            <StackLayout Orientation="Horizontal">
                <Label Text="{Binding Likes}" />
                <Label Text="{Binding Comments}" />
            </StackLayout>
        </StackLayout>
    </StackLayout>
</StackLayout>

Μετρήστε τα — 7 StackLayouts σε ένα μόνο component. Αυτό πονάει.

Καλή Πρακτική: Flat Grid Layout

<!-- GOOD: Flat Grid replaces multiple nested StackLayouts -->
<Grid RowDefinitions="Auto,Auto,Auto"
      ColumnDefinitions="48,*,Auto"
      Padding="12"
      RowSpacing="4"
      ColumnSpacing="8">

    <!-- Row 0: Name and Date -->
    <Label Grid.Row="0" Grid.Column="1"
           Text="{Binding Name}"
           FontAttributes="Bold" />
    <Label Grid.Row="0" Grid.Column="2"
           Text="{Binding Date}"
           HorizontalOptions="End"
           TextColor="Gray" />

    <!-- Row 1: Avatar spanning rows, Title and Status -->
    <Image Grid.Row="0" Grid.Column="0" Grid.RowSpan="3"
           Source="{Binding Avatar}"
           WidthRequest="48"
           HeightRequest="48"
           Aspect="AspectFill" />
    <Label Grid.Row="0" Grid.Column="2"
           Text="{Binding Status}" />

    <!-- Row 1: Description -->
    <Label Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"
           Text="{Binding Description}"
           LineBreakMode="TailTruncation" />

    <!-- Row 2: Likes and Comments -->
    <Label Grid.Row="2" Grid.Column="1"
           Text="{Binding Likes, StringFormat='♥ {0}'}" />
    <Label Grid.Row="2" Grid.Column="2"
           Text="{Binding Comments, StringFormat='💬 {0}'}" />
</Grid>

Ένα Grid, μηδέν nesting. Πολύ καλύτερα.

Επιπλέον Συμβουλές για Layouts

  • Προτιμήστε Grid αντί για εμφωλευμένα StackLayout — ένα Grid μπορεί να αντικαταστήσει πολλά επίπεδα StackLayout
  • Αποφεύγετε τα AbsoluteLayout και RelativeLayout εκτός αν χρειάζονται πραγματικά — είναι πιο αργά στον υπολογισμό
  • Χρησιμοποιήστε VerticalStackLayout και HorizontalStackLayout αντί για StackLayout — οι ειδικευμένες εκδόσεις είναι πιο ελαφριές
  • Ορίστε IsVisible="False" σε στοιχεία που δεν εμφανίζονται, ώστε να εξαιρούνται από τον υπολογισμό layout
  • Αντικαταστήστε margins/paddings με spacing στο parent layout όπου γίνεται

Διαχείριση Μνήμης

Σε κινητές συσκευές οι πόροι είναι περιορισμένοι — αυτό δεν θα αλλάξει ποτέ. Memory leaks, υπερβολική κατανάλωση μνήμης και πόροι που δεν απελευθερώνονται σωστά οδηγούν σε crashes και (σίγουρα) κακές κριτικές στα app stores.

Lazy Initialization

Μη δημιουργείτε αντικείμενα πριν τα χρειαστείτε. Η lazy initialization είναι ο φίλος σας:

public class ProductService
{
    // Lazy initialization - database created only when first accessed
    private readonly Lazy<SQLiteAsyncConnection> _database;

    // Lazy initialization for expensive resources
    private readonly Lazy<HttpClient> _httpClient;

    public ProductService()
    {
        _database = new Lazy<SQLiteAsyncConnection>(() =>
        {
            var dbPath = Path.Combine(
                FileSystem.AppDataDirectory, "products.db");
            return new SQLiteAsyncConnection(dbPath);
        });

        _httpClient = new Lazy<HttpClient>(() =>
        {
            var handler = new HttpClientHandler
            {
                AutomaticDecompression =
                    DecompressionMethods.GZip | DecompressionMethods.Deflate
            };
            return new HttpClient(handler)
            {
                Timeout = TimeSpan.FromSeconds(30)
            };
        });
    }

    public SQLiteAsyncConnection Database => _database.Value;
    public HttpClient Http => _httpClient.Value;
}

Σωστή Διαχείριση Εικόνων

Οι εικόνες είναι (σχεδόν πάντα) ο νούμερο ένα λόγος υψηλής κατανάλωσης μνήμης. Χρειάζεστε caching και compression:

public class ImageOptimizationService
{
    // Use a memory cache with size limits
    private readonly MemoryCache _imageCache = new(new MemoryCacheOptions
    {
        SizeLimit = 50 * 1024 * 1024 // 50MB max cache
    });

    public async Task<ImageSource> GetOptimizedImageAsync(
        string url, int maxWidth = 300, int maxHeight = 300)
    {
        if (_imageCache.TryGetValue(url, out ImageSource cached))
            return cached;

        using var httpClient = new HttpClient();
        var imageData = await httpClient.GetByteArrayAsync(url);

        // Resize image to reduce memory footprint
        var resizedData = ResizeImage(imageData, maxWidth, maxHeight);

        var imageSource = ImageSource.FromStream(
            () => new MemoryStream(resizedData));

        var cacheOptions = new MemoryCacheEntryOptions()
            .SetSize(resizedData.Length)
            .SetSlidingExpiration(TimeSpan.FromMinutes(10));

        _imageCache.Set(url, imageSource, cacheOptions);

        return imageSource;
    }

    private byte[] ResizeImage(
        byte[] imageData, int maxWidth, int maxHeight)
    {
        // Platform-specific image resizing implementation
        // Use SkiaSharp or platform APIs
        return imageData;
    }
}

IDisposable Pattern και Weak References

Ο σωστός χειρισμός του IDisposable pattern και η χρήση weak references για event handlers είναι κρίσιμα για την αποφυγή memory leaks:

public class MyPageViewModel : ObservableObject, IDisposable
{
    private readonly IMessenger _messenger;
    private bool _disposed;

    public MyPageViewModel(IMessenger messenger)
    {
        _messenger = messenger;

        // Use WeakReferenceMessenger to avoid memory leaks
        WeakReferenceMessenger.Default.Register<DataChangedMessage>(
            this, (recipient, message) =>
            {
                ((MyPageViewModel)recipient).OnDataChanged(message);
            });
    }

    private void OnDataChanged(DataChangedMessage message)
    {
        // Handle the message
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // Unregister from messenger
                WeakReferenceMessenger.Default.UnregisterAll(this);

                // Dispose managed resources
            }
            _disposed = true;
        }
    }
}

// In your page, handle appearing/disappearing
public partial class MyPage : ContentPage
{
    protected override void OnDisappearing()
    {
        base.OnDisappearing();

        // Clean up when page is no longer visible
        if (BindingContext is IDisposable disposable)
        {
            disposable.Dispose();
        }
    }
}

Αποφυγή Memory Leaks σε Event Handlers

Τα event subscriptions που ξεχνάμε να αφαιρέσουμε είναι μία από τις πιο ύπουλες αιτίες memory leaks. Χρησιμοποιήστε WeakEventManager:

public class SensorService
{
    // Use WeakEventManager to prevent memory leaks
    private readonly WeakEventManager _eventManager = new();

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

    protected void OnDataReceived(SensorDataEventArgs args)
    {
        _eventManager.HandleEvent(this, args, nameof(DataReceived));
    }
}

Async Programming και ConfigureAwait

Κάθε blocking call στο main thread σημαίνει frozen UI. Στο .NET MAUI, η σωστή χρήση async/await μπορεί να κάνει τη διαφορά ανάμεσα σε μια εφαρμογή που "ρέει" και μια που κολλάει.

Χρήση ConfigureAwait(false)

Όταν δεν χρειάζεστε πρόσβαση στο UI thread μετά από ένα await, βάλτε ConfigureAwait(false). Αποφεύγετε έτσι την περιττή επιστροφή στο SynchronizationContext:

public class DataService
{
    private readonly HttpClient _httpClient;

    public async Task<List<Product>> GetProductsAsync()
    {
        // ConfigureAwait(false) - we don't need the UI thread here
        var response = await _httpClient
            .GetAsync("api/products")
            .ConfigureAwait(false);

        response.EnsureSuccessStatusCode();

        var json = await response.Content
            .ReadAsStringAsync()
            .ConfigureAwait(false);

        // Deserialization doesn't need UI thread
        var products = JsonSerializer.Deserialize<List<Product>>(
            json, AppJsonContext.Default.ListProduct);

        return products;
    }
}

// In ViewModel - only return to UI thread when updating UI
public class ProductsViewModel : ObservableObject
{
    private readonly DataService _dataService;

    [ObservableProperty]
    private ObservableCollection<Product> _products;

    [ObservableProperty]
    private bool _isLoading;

    public async Task LoadProductsAsync()
    {
        IsLoading = true;
        try
        {
            // Data loading happens off the UI thread
            var products = await _dataService
                .GetProductsAsync()
                .ConfigureAwait(false);

            // Return to UI thread only for UI updates
            await MainThread.InvokeOnMainThreadAsync(() =>
            {
                Products = new ObservableCollection<Product>(products);
            });
        }
        catch (Exception ex)
        {
            await MainThread.InvokeOnMainThreadAsync(() =>
            {
                // Show error on UI thread
                Shell.Current.DisplayAlert("Error", ex.Message, "OK");
            });
        }
        finally
        {
            IsLoading = false;
        }
    }
}

Αποφυγή Blocking Calls

Αυτό θα το πω όσο πιο ξεκάθαρα γίνεται: ποτέ μην χρησιμοποιείτε .Result ή .Wait() σε async μεθόδους. Deadlocks σε 3... 2... 1...

// BAD: Blocking the UI thread - can cause deadlocks
public void LoadData()
{
    // NEVER do this in MAUI!
    var data = _dataService.GetDataAsync().Result;
    var items = _dataService.GetItemsAsync().GetAwaiter().GetResult();
}

// GOOD: Proper async pattern
public async Task LoadDataAsync()
{
    var data = await _dataService.GetDataAsync();
    var items = await _dataService.GetItemsAsync();
}

// GOOD: Fire-and-forget with error handling for event handlers
public void OnButtonClicked(object sender, EventArgs e)
{
    _ = LoadDataSafeAsync();
}

private async Task LoadDataSafeAsync()
{
    try
    {
        await LoadDataAsync();
    }
    catch (Exception ex)
    {
        Debug.WriteLine($"Error loading data: {ex.Message}");
    }
}

Παράλληλη Φόρτωση Δεδομένων

Χρειάζεστε πολλαπλά δεδομένα κατά την εκκίνηση μιας σελίδας; Μη τα φορτώνετε σειριακά — φορτώστε τα παράλληλα:

public async Task InitializeAsync()
{
    // Load all data in parallel instead of sequentially
    var productsTask = _dataService.GetProductsAsync();
    var categoriesTask = _dataService.GetCategoriesAsync();
    var userProfileTask = _dataService.GetUserProfileAsync();

    // Wait for all to complete
    await Task.WhenAll(productsTask, categoriesTask, userProfileTask)
        .ConfigureAwait(false);

    // Access results
    var products = productsTask.Result;
    var categories = categoriesTask.Result;
    var userProfile = userProfileTask.Result;

    await MainThread.InvokeOnMainThreadAsync(() =>
    {
        Products = new ObservableCollection<Product>(products);
        Categories = new ObservableCollection<Category>(categories);
        UserProfile = userProfile;
    });
}

Η διαφορά μπορεί να είναι δραματική. Αν κάθε κλήση παίρνει 500ms, σειριακά θα χρειαστείτε 1.5 δευτερόλεπτα. Παράλληλα; Μόνο ~500ms.

Trimming και Μείωση Μεγέθους Εφαρμογής

Το μέγεθος της εφαρμογής μετράει — επηρεάζει τον χρόνο λήψης, τον αποθηκευτικό χώρο, ακόμα και το αν κάποιος θα κατεβάσει την εφαρμογή σας με mobile data. Το .NET MAUI διαθέτει εργαλεία trimming που αφαιρούν τον αχρησιμοποίητο κώδικα.

Ενεργοποίηση IL Trimmer

Ο IL Trimmer (ή tree shaker, αν προτιμάτε) αναλύει τον κώδικά σας και αφαιρεί τύπους, μεθόδους και assemblies που δεν χρησιμοποιούνται. Η ρύθμιση στο .csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
    <OutputType>Exe</OutputType>
    <UseMaui>true</UseMaui>

    <!-- Enable trimming for release builds -->
    <PublishTrimmed>true</PublishTrimmed>

    <!-- Trimming granularity: 'link' is most aggressive -->
    <TrimMode>link</TrimMode>

    <!-- Enable single-file publishing -->
    <PublishSingleFile>true</PublishSingleFile>

    <!-- Compress assemblies for smaller APK/IPA -->
    <EnableCompression>true</EnableCompression>

    <!-- Android-specific optimizations -->
    <AndroidLinkMode>SdkOnly</AndroidLinkMode>
    <AndroidEnableProfiledAot>true</AndroidEnableProfiledAot>
  </PropertyGroup>

  <!-- Preserve assemblies that use reflection -->
  <ItemGroup>
    <TrimmerRootAssembly Include="MyApp.ViewModels" />
  </ItemGroup>
</Project>

Android-Specific Βελτιστοποιήσεις

Αν στοχεύετε Android, υπάρχουν κάποιες επιπλέον βελτιστοποιήσεις που αξίζει να ενεργοποιήσετε:

<PropertyGroup Condition="'$(TargetFramework)' == 'net10.0-android'">
    <!-- Use AOT compilation for Android -->
    <RunAOTCompilation>true</RunAOTCompilation>

    <!-- Enable R8 code shrinking (replaces ProGuard) -->
    <AndroidEnableR8>true</AndroidEnableR8>

    <!-- Generate only required ABI architectures -->
    <RuntimeIdentifiers>android-arm64</RuntimeIdentifiers>

    <!-- Strip debug symbols in release -->
    <DebugSymbols>false</DebugSymbols>
    <DebugType>none</DebugType>
</PropertyGroup>

Μείωση Μεγέθους Πόρων

Πέρα από τον κώδικα, μην ξεχνάτε τους πόρους:

  • Χρησιμοποιήστε SVG αντί για PNG/JPEG όπου γίνεται — το .NET MAUI τα μετατρέπει αυτόματα στο σωστό format
  • Συμπιέστε τις εικόνες πριν τις βάλετε στο project (εργαλεία όπως TinyPNG κάνουν θαύματα)
  • Αφαιρέστε αχρησιμοποίητα fonts και resources
  • Χρησιμοποιήστε font icons αντί για εικόνες σε μικρά εικονίδια — ένα font file αντικαθιστά δεκάδες images

Shell Navigation για Καλύτερη Εκκίνηση

Η αρχιτεκτονική πλοήγησης επηρεάζει σημαντικά τον χρόνο εκκίνησης. Με το .NET MAUI Shell, οι σελίδες δημιουργούνται lazy — μόνο όταν ο χρήστης πλοηγηθεί σε αυτές. Αυτό σημαίνει ταχύτερη εκκίνηση, χωρίς να πληρώνετε για σελίδες που ο χρήστης μπορεί να μην δει ποτέ.

Lazy Loading με Shell

Αντί να δηλώνετε σελίδες ως απευθείας tabs, χρησιμοποιήστε route registration:

<!-- AppShell.xaml - Only main tabs load at startup -->
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:views="clr-namespace:MyApp.Views">

    <!-- These pages are created at startup -->
    <TabBar>
        <ShellContent Title="Home"
                      Icon="home.png"
                      ContentTemplate="{DataTemplate views:HomePage}" />
        <ShellContent Title="Search"
                      Icon="search.png"
                      ContentTemplate="{DataTemplate views:SearchPage}" />
        <ShellContent Title="Profile"
                      Icon="profile.png"
                      ContentTemplate="{DataTemplate views:ProfilePage}" />
    </TabBar>
</Shell>

Προσέξτε τη χρήση ContentTemplate αντί για Content — αυτό είναι το κλειδί. Η σελίδα δημιουργείται μόνο όταν ο χρήστης πατήσει το tab.

Route Registration για Secondary Σελίδες

Οι δευτερεύουσες σελίδες καταχωρούνται ως routes και δημιουργούνται on-demand:

// In AppShell.xaml.cs or MauiProgram.cs
public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();

        // Register routes - pages created only when navigated to
        Routing.RegisterRoute("productdetails", typeof(ProductDetailsPage));
        Routing.RegisterRoute("checkout", typeof(CheckoutPage));
        Routing.RegisterRoute("orderhistory", typeof(OrderHistoryPage));
        Routing.RegisterRoute("settings", typeof(SettingsPage));
    }
}

// Navigate to a page - created on demand
await Shell.Current.GoToAsync("productdetails", new Dictionary<string, object>
{
    ["Product"] = selectedProduct
});

Deferred Content Loading

Για σελίδες με πολύ περιεχόμενο, η σελίδα εμφανίζεται αμέσως (με loading state) και τα δεδομένα φορτώνονται μετά:

public partial class ProductDetailsPage : ContentPage
{
    private readonly ProductDetailsViewModel _viewModel;

    public ProductDetailsPage(ProductDetailsViewModel viewModel)
    {
        InitializeComponent();
        BindingContext = _viewModel = viewModel;
    }

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

        // Page appears immediately with loading state
        // Data loads asynchronously after
        if (!_viewModel.IsInitialized)
        {
            await _viewModel.InitializeAsync();
        }
    }

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

        // Release resources when page is not visible
        _viewModel.OnNavigatedFrom();
    }
}

Dependency Injection Optimization

Το .NET MAUI χρησιμοποιεί τον built-in DI container του .NET. Μπορεί να ακούγεται "βαρετό" θέμα, αλλά η σωστή ρύθμιση του DI container επηρεάζει τόσο τον χρόνο εκκίνησης όσο και τη χρήση μνήμης.

Transient vs Singleton vs Scoped

Η επιλογή του σωστού lifetime για κάθε service κάνει μεγάλη διαφορά:

  • Singleton: Δημιουργείται μία φορά, μοιράζεται παντού. Ιδανικό για HttpClient, database connections, caching
  • Transient: Νέο instance κάθε φορά. Κατάλληλο για lightweight, stateful services
  • Scoped: Ένα instance ανά scope. Σπάνια χρήσιμο στο MAUI, ειλικρινά
public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });

        // === SINGLETON services (created once, shared everywhere) ===

        // HttpClient should be singleton to reuse connections
        builder.Services.AddSingleton<HttpClient>(sp =>
        {
            var handler = new HttpClientHandler
            {
                AutomaticDecompression =
                    DecompressionMethods.GZip | DecompressionMethods.Deflate
            };
            return new HttpClient(handler)
            {
                BaseAddress = new Uri("https://api.myapp.com/"),
                Timeout = TimeSpan.FromSeconds(30)
            };
        });

        // Database connection - singleton
        builder.Services.AddSingleton<IDatabase, SqliteDatabase>();

        // Caching service - singleton
        builder.Services.AddSingleton<ICacheService, MemoryCacheService>();

        // Settings/Preferences - singleton
        builder.Services.AddSingleton<ISettingsService, SettingsService>();

        // Navigation service - singleton
        builder.Services.AddSingleton<INavigationService, ShellNavigationService>();

        // === TRANSIENT services (new instance each time) ===

        // Data services that might hold request-specific state
        builder.Services.AddTransient<IProductService, ProductService>();
        builder.Services.AddTransient<IOrderService, OrderService>();

        // === ViewModels - Transient for pages, Singleton for tabs ===

        // Tab ViewModels as Singleton (tabs persist)
        builder.Services.AddSingleton<HomeViewModel>();
        builder.Services.AddSingleton<SearchViewModel>();

        // Page ViewModels as Transient (new state each navigation)
        builder.Services.AddTransient<ProductDetailsViewModel>();
        builder.Services.AddTransient<CheckoutViewModel>();

        // === Pages ===
        builder.Services.AddSingleton<HomePage>();
        builder.Services.AddSingleton<SearchPage>();
        builder.Services.AddTransient<ProductDetailsPage>();
        builder.Services.AddTransient<CheckoutPage>();

        return builder.Build();
    }
}

Αποφυγή Περιττών Registrations

Καταχωρήστε μόνο ό,τι χρειάζεστε. Κάθε registration κοστίζει σε χρόνο εκκίνησης και μνήμη:

// BAD: Registering services that might not be needed
builder.Services.AddSingleton<IAnalyticsService, AnalyticsService>();
builder.Services.AddSingleton<ICrashReporter, CrashReporter>();
builder.Services.AddSingleton<IRemoteConfig, RemoteConfigService>();
builder.Services.AddSingleton<IABTesting, ABTestingService>();
builder.Services.AddSingleton<IPushNotifications, PushService>();
// ... 50 more services

// GOOD: Register only what you need, use lazy loading for optional services
builder.Services.AddSingleton<IAnalyticsService, AnalyticsService>();

// Use Lazy<T> for services not needed at startup
builder.Services.AddSingleton<Lazy<ICrashReporter>>(sp =>
    new Lazy<ICrashReporter>(() =>
        new CrashReporter(sp.GetRequiredService<ILogger>())));

// Consider feature flags to conditionally register services
if (FeatureFlags.PushNotificationsEnabled)
{
    builder.Services.AddSingleton<IPushNotifications, PushService>();
}

Χρήση Keyed Services (.NET 8+)

Από το .NET 8+, μπορείτε να χρησιμοποιήσετε keyed services — κάτι πολύ χρήσιμο όταν έχετε πολλαπλά implementations του ίδιου interface:

// Register different implementations with keys
builder.Services.AddKeyedSingleton<IApiClient, ProductionApiClient>("prod");
builder.Services.AddKeyedSingleton<IApiClient, StagingApiClient>("staging");

// Inject specific implementation
public class MyService(
    [FromKeyedServices("prod")] IApiClient apiClient)
{
    // Uses the production API client
}

Profiling και Μέτρηση Απόδοσης

Βελτιστοποίηση χωρίς μέτρηση δεν είναι βελτιστοποίηση — είναι εικασία. Πριν αγγίξετε οτιδήποτε, μετρήστε. Εντοπίστε τα πραγματικά bottlenecks. Και μετά, επαληθεύστε ότι οι αλλαγές σας έκαναν όντως διαφορά.

dotnet-trace

Το dotnet-trace είναι ένα cross-platform εργαλείο για .NET traces. Δουλεύει και με Android και iOS:

# Install dotnet-trace
dotnet tool install --global dotnet-trace

# Collect a trace from a running app (Android via USB)
dotnet-trace collect --process-id <PID> --output app-trace.nettrace

# Convert trace to SpeedScope format for web-based visualization
dotnet-trace convert app-trace.nettrace --format speedscope

Ενσωματωμένο Startup Tracing

Θέλετε να ξέρετε πόσο χρόνο παίρνει η εκκίνηση; Δημιουργήστε custom diagnostics:

public partial class App : Application
{
    private static readonly Stopwatch StartupTimer = Stopwatch.StartNew();

    public App()
    {
        InitializeComponent();

        Debug.WriteLine($"App constructor: {StartupTimer.ElapsedMilliseconds}ms");
    }

    protected override Window CreateWindow(IActivationState state)
    {
        Debug.WriteLine(
            $"CreateWindow: {StartupTimer.ElapsedMilliseconds}ms");

        return new Window(new AppShell());
    }
}

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
    }

    protected override void OnAppearing()
    {
        base.OnAppearing();
        Debug.WriteLine(
            $"MainPage appeared: {App.StartupTimer.ElapsedMilliseconds}ms");
        App.StartupTimer.Stop();
    }
}

Xcode Instruments (iOS)

Για iOS, τα Xcode Instruments είναι ανεκτίμητα. Τα πιο χρήσιμα για MAUI εφαρμογές:

  • Time Profiler: Εντοπίζει τις πιο αργές μεθόδους
  • Allocations: Παρακολουθεί εκχωρήσεις μνήμης και βοηθά στον εντοπισμό leaks
  • Leaks: Ανιχνεύει memory leaks σε πραγματικό χρόνο
  • Core Animation: Μετρά FPS και εντοπίζει dropped frames
  • Energy Log: Μετρά κατανάλωση ενέργειας

Android Profiler

Για Android, τα εργαλεία του Android Studio:

  • CPU Profiler: Ανάλυση CPU, method tracing, flame charts
  • Memory Profiler: Παρακολούθηση heap allocations, εντοπισμός leaks
  • Network Profiler: Ανάλυση δικτυακών κλήσεων και latency
  • Energy Profiler: Εκτίμηση κατανάλωσης μπαταρίας

Custom Performance Monitoring

Για κρίσιμες λειτουργίες, φτιάξτε custom performance monitors. Κάτι σαν αυτό:

public class PerformanceMonitor
{
    private readonly ConcurrentDictionary<string, Stopwatch> _timers = new();
    private readonly ILogger<PerformanceMonitor> _logger;

    public PerformanceMonitor(ILogger<PerformanceMonitor> logger)
    {
        _logger = logger;
    }

    public IDisposable TrackOperation(string operationName)
    {
        return new OperationTracker(operationName, _logger);
    }

    private class OperationTracker : IDisposable
    {
        private readonly string _name;
        private readonly Stopwatch _stopwatch;
        private readonly ILogger _logger;

        public OperationTracker(string name, ILogger logger)
        {
            _name = name;
            _logger = logger;
            _stopwatch = Stopwatch.StartNew();
            _logger.LogDebug("Starting operation: {Operation}", name);
        }

        public void Dispose()
        {
            _stopwatch.Stop();
            _logger.LogInformation(
                "Operation {Operation} completed in {ElapsedMs}ms",
                _name, _stopwatch.ElapsedMilliseconds);

            // Warn if operation took too long
            if (_stopwatch.ElapsedMilliseconds > 1000)
            {
                _logger.LogWarning(
                    "Slow operation detected: {Operation} took {ElapsedMs}ms",
                    _name, _stopwatch.ElapsedMilliseconds);
            }
        }
    }
}

// Usage in ViewModel
public async Task LoadDataAsync()
{
    using (_perfMonitor.TrackOperation("LoadProductsPage"))
    {
        var products = await _productService.GetAllAsync();
        Products = new ObservableCollection<Product>(products);
    }
}

Benchmark με BenchmarkDotNet

Για ακριβείς μετρήσεις σε συγκεκριμένα κομμάτια κώδικα, το BenchmarkDotNet είναι η ιδανική επιλογή:

[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net100)]
public class BindingBenchmarks
{
    [Benchmark(Baseline = true)]
    public void ReflectionBinding()
    {
        // Traditional binding via reflection
        var binding = new Binding("PropertyName");
    }

    [Benchmark]
    public void CompiledBinding()
    {
        // Compiled binding - direct property access
        var binding = new Binding(
            nameof(MyViewModel.PropertyName),
            source: _viewModel);
    }
}

Συμπεράσματα

Η βελτιστοποίηση απόδοσης στο .NET MAUI δεν είναι ένα πράγμα — είναι ένα σύνολο πρακτικών που καλύπτουν τα πάντα, από compiled bindings μέχρι διαχείριση μνήμης και deployment options. Τα .NET 9 και .NET 10 κάνουν το MAUI πιο ελκυστικό από ποτέ για production εφαρμογές.

Ακολουθεί ένα γρήγορο checklist με τα πιο σημαντικά.

Checklist Βελτιστοποίησης Απόδοσης

  1. Compiled Bindings: Χρησιμοποιήστε x:DataType παντού — κέρδος 8x-20x στα bindings
  2. NativeAOT: Ενεργοποιήστε για iOS/Mac Catalyst — 2x ταχύτερη εκκίνηση, μικρότερο μέγεθος
  3. CollectionView: Μην το βάζετε σε ScrollView, χρησιμοποιήστε σταθερό ύψος, compiled bindings στα DataTemplates
  4. Flat Layouts: Grid αντί για εμφωλευμένα StackLayouts, VerticalStackLayout/HorizontalStackLayout
  5. Διαχείριση Μνήμης: Lazy initialization, image caching, IDisposable, weak references
  6. Async Patterns: ConfigureAwait(false) σε non-UI κώδικα, μηδέν blocking calls, παράλληλη φόρτωση
  7. Trimming: PublishTrimmed και TrimMode=link για μικρότερα binaries
  8. Shell Navigation: ContentTemplate για lazy loading, routes αντί για direct page references
  9. DI Optimization: Σωστό lifetime (Singleton vs Transient), αποφυγή περιττών registrations
  10. Profiling: Μετρήστε πριν βελτιστοποιήσετε — dotnet-trace, Xcode Instruments, Android Profiler

Ο κανόνας-χρυσός; Μετρήστε πρώτα, βελτιστοποιήστε μετά. Μην υποθέτετε πού βρίσκονται τα bottlenecks — τα profiling δεδομένα θα σας πουν. Ξεκινήστε από τις αλλαγές με τη μεγαλύτερη επίδραση (compiled bindings, NativeAOT, σωστό CollectionView) και μετά προχωρήστε σε λεπτομέρειες μόνο αν χρειάζεται.

Με τις βελτιώσεις του .NET 10 — καλύτερο NativeAOT, βελτιωμένα compiled bindings, αποδοτικότεροι handlers, μικρότερο memory footprint — το .NET MAUI είναι πλέον μια ώριμη πλατφόρμα για cross-platform ανάπτυξη. Εφαρμόστε τις τεχνικές αυτού του οδηγού, μετρήστε τα αποτελέσματα, και θα δείτε πραγματική διαφορά στις εφαρμογές σας.

Σχετικά με τον Συγγραφέα Editorial Team

Our team of expert writers and editors.