الدليل الشامل لتحسين أداء تطبيقات .NET MAUI: من سرعة الإقلاع إلى إدارة الذاكرة

دليل عملي لتحسين أداء تطبيقات .NET MAUI يغطي الربط المُجمَّع، التجميع المسبق AOT، تقليم الكود، تحسين القوائم، إدارة الذاكرة ومنع التسريبات، وأدوات التوصيف — مع كود جاهز للتطبيق.

لنكن صريحين — بناء تطبيق بـ .NET MAUI ليس فقط عن المظهر الجميل والوظائف الذكية. الأداء هو ذلك العامل الخفي الذي يقرر مصير تطبيقك: هل سيبقى المستخدم أم سيحذفه خلال ثوانٍ معدودة؟ تطبيق بطيء في الإقلاع، يتجمد أثناء التمرير، أو يلتهم الذاكرة بشراهة — أي من هذه كافية لخسارة مستخدميك.

الخبر الجيد؟ .NET MAUI يوفر ترسانة حقيقية من الأدوات والتقنيات لتحسين الأداء بشكل ملحوظ. من التجميع المسبق (AOT) إلى الربط المُجمَّع (Compiled Bindings)، ومن تقليم الكود إلى إدارة الذاكرة الذكية — هناك الكثير مما يمكنك فعله، وأكثره أسهل مما تتخيل.

في هذا الدليل، سنمر على كل جانب من جوانب تحسين الأداء مع أمثلة عملية وكود جاهز للتطبيق. سواء كنت تبني تطبيقاً جديداً من الصفر أو تحاول إنقاذ تطبيق قائم يعاني من البطء، ستجد هنا ما يفيدك.

الربط المُجمَّع (Compiled Bindings): تسريع يصل إلى 20 ضعفاً

لنبدأ بأحد أكثر التحسينات تأثيراً وأسهلها تطبيقاً: الربط المُجمَّع. في الوضع الافتراضي، يستخدم .NET MAUI الانعكاس (Reflection) لحل تعبيرات الربط أثناء وقت التشغيل — يعني ببساطة أن النظام يبحث عن الخصائص والأوامر ديناميكياً في كل مرة، وهذا بطيء بشكل مؤلم.

مع الربط المُجمَّع، يتم حل كل شيء أثناء الترجمة (Compile-time)، فيُلغى الانعكاس تماماً. والنتائج؟ صراحةً مبهرة:

  • ربط OneWay أو TwoWay يصبح أسرع بحوالي 8 أضعاف
  • ربط OneTime يصبح أسرع بحوالي 20 ضعفاً (نعم، عشرين!)
  • ضبط BindingContext يصبح أسرع بحوالي 5 أضعاف

كيف تفعّل الربط المُجمَّع؟

الموضوع أبسط مما تتوقع. كل ما تحتاجه هو إضافة سمة x:DataType في XAML لتحديد نوع الكائن:

<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:DataType="viewmodels:ProductViewModel">

    <VerticalStackLayout Padding="16">
        <Label Text="{Binding Name}"
               FontSize="24"
               FontAttributes="Bold" />

        <Label Text="{Binding FormattedPrice}"
               FontSize="18"
               TextColor="Green" />

        <Button Text="إضافة للسلة"
                Command="{Binding AddToCartCommand}" />
    </VerticalStackLayout>
</ContentPage>

الجميل في الموضوع أنه عندما تحدد x:DataType، المُترجم يتحقق من وجود الخصائص أثناء البناء. يعني لو كتبت اسم خاصية غلط، تحصل على خطأ ترجمة واضح بدلاً من فشل صامت أثناء التشغيل — وهذا وحده يستحق التبديل.

الربط المُجمَّع في DataTemplates

عند استخدام DataTemplate داخل CollectionView، لا تنسَ تحديد x:DataType على مستوى القالب نفسه:

<CollectionView ItemsSource="{Binding Products}">
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="models:Product">
            <Grid Padding="8" ColumnDefinitions="*,Auto">
                <Label Text="{Binding Name}" />
                <Label Text="{Binding Price, StringFormat='{0:C}'}"
                       Grid.Column="1" />
            </Grid>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

الربط المُجمَّع في الكود (C#)

بدءاً من .NET MAUI 9، صار بإمكانك استخدام تعبيرات Lambda بدلاً من السلاسل النصية. هذا يعني type-safety كاملة وأداء أفضل:

// الطريقة الجديدة باستخدام Lambda — مُجمَّعة تلقائياً
var label = new Label();
label.SetBinding(Label.TextProperty,
    static (ProductViewModel vm) => vm.Name);

// أو باستخدام BindingBase.Create
var binding = BindingBase.Create(
    static (ProductViewModel vm) => vm.FormattedPrice,
    mode: BindingMode.OneWay);
label.SetBinding(Label.TextProperty, binding);

ملاحظة تستحق الانتباه: بدءاً من .NET MAUI 9، المُترجم يعرض تحذيرات للربطات غير المُجمَّعة. هذا تلميح واضح أن مايكروسوفت تتجه لجعل الربط المُجمَّع هو الوضع الافتراضي — فالأفضل أن تسبقهم إليه.

تسطيح هيكل الواجهة: كل طبقة تداخل لها ثمن

هذه نقطة يغفل عنها كثير من المطورين. كل مستوى تداخل في واجهتك يعني حسابات إضافية للتخطيط والعرض. والمشكلة أن التداخل المفرط يتراكم بهدوء دون أن تلاحظه حتى يصبح التطبيق ثقيلاً.

المشكلة: التداخل المفرط

<!-- ❌ سيء: تداخل مفرط -->
<StackLayout>
    <StackLayout Orientation="Horizontal">
        <StackLayout>
            <Label Text="{Binding Name}" />
            <Label Text="{Binding Description}" />
        </StackLayout>
        <StackLayout>
            <Label Text="{Binding Price}" />
            <Button Text="شراء" />
        </StackLayout>
    </StackLayout>
</StackLayout>

الحل: استخدام Grid مسطح

<!-- ✅ أفضل: Grid مسطح -->
<Grid RowDefinitions="Auto,Auto"
      ColumnDefinitions="*,Auto"
      Padding="8">

    <Label Text="{Binding Name}"
           Grid.Row="0" Grid.Column="0"
           FontAttributes="Bold" />

    <Label Text="{Binding Description}"
           Grid.Row="1" Grid.Column="0" />

    <Label Text="{Binding Price}"
           Grid.Row="0" Grid.Column="1"
           VerticalOptions="Center" />

    <Button Text="شراء"
            Grid.Row="1" Grid.Column="1" />
</Grid>

القاعدة بسيطة: إذا وجدت نفسك تتجاوز 3 مستويات تداخل، توقف وأعد التفكير. Grid هو أفضل صديق لك هنا — يسمح لك بترتيب العناصر في صفوف وأعمدة بدون أي تداخل. من تجربتي، التحويل من StackLayout متداخل إلى Grid مسطح يُحدث فرقاً ملحوظاً في سلاسة التمرير.

التجميع المسبق (Native AOT) وتقليم الكود (Trimming)

هنا ندخل في المنطقة الجدية. التجميع المسبق الأصلي (Native AOT) هو من أقوى التحسينات المتاحة، خاصةً على iOS و macOS. الفكرة ببساطة: بدلاً من ترجمة الكود أثناء التشغيل عبر JIT، يتم تحويله لكود أصلي خاص بالمنصة أثناء البناء.

الفوائد الحقيقية

الأرقام التي تنشرها مايكروسوفت ملموسة وقابلة للقياس:

  • حجم التطبيق ينخفض بنسبة تصل إلى 35%
  • زمن الإقلاع ينخفض بنسبة تصل إلى 28%
  • أداء وقت التشغيل يتحسن بنسبة تصل إلى 50%
  • التطبيق يصبح أصغر بـ 2.5 ضعف ويقلع بسرعة مضاعفة

أرقام مغرية، أليس كذلك؟

تفعيل Native AOT

لتفعيله، أضف هذه الإعدادات في ملف المشروع .csproj:

<PropertyGroup>
    <!-- تفعيل توافق AOT لجميع المنصات -->
    <IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

<!-- للنشر مع Native AOT (iOS و macOS) -->
<PropertyGroup Condition="$(TargetFramework.Contains('ios')) Or $(TargetFramework.Contains('maccatalyst'))">
    <PublishAot>true</PublishAot>
</PropertyGroup>

ضبط IsAotCompatible يفعّل محللات التقليم وAOT التي تنبهك للكود غير المتوافق أثناء التطوير — وهذا يوفر عليك كثيراً من الصداع لاحقاً.

ماذا عن Android؟

للأسف، Android لا يدعم Native AOT الكامل حالياً. لكن لا تقلق — هناك بديل فعّال اسمه Profiled AOT. الفكرة أنه يجمّع مسار الإقلاع فقط بشكل مسبق، فيتحسن زمن الإقلاع بشكل ملحوظ مع زيادة طفيفة جداً في حجم التطبيق. والأجمل أنه مفعّل افتراضياً في بناءات Release منذ .NET 6:

<PropertyGroup Condition="$(TargetFramework.Contains('android'))">
    <!-- مفعّل افتراضياً في Release -->
    <RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>

القيود والمحاذير

لكن (ودائماً هناك "لكن") Native AOT يأتي مع قيود مهمة:

  • لا يمكن استخدام الانعكاس (Reflection) بحرية — يجب استخدام Annotations لأنماط الانعكاس
  • لا يمكن استخدام الكود الديناميكي مثل System.Linq.Expressions
  • لا يمكن تحميل مكتبات (Assemblies) أثناء التشغيل
  • المكتبات الخارجية يجب أن تكون متوافقة مع AOT — وهذا ليس مضموناً دائماً

نصيحة مهمة: عند تفعيل Native AOT، راجع جميع تحذيرات التقليم وAOT بعناية شديدة. تحذير واحد يبدو تافهاً قد يعني أن التطبيق لن يعمل بشكل صحيح في الإنتاج.

ترجمة XAML المُجمَّعة: الأساس الذي يُبنى عليه كل شيء

ترجمة XAML المُجمَّعة هي القطعة التي تربط كل ما سبق. بدلاً من تفسير XAML أثناء التشغيل، يتم تحويله لكود فعلي لدالة InitializeComponent() أثناء البناء.

في .NET MAUI 9، أكملت مايكروسوفت أخيراً دعم جميع ميزات XAML التي كان المُترجم يعجز عن التعامل معها — بما في ذلك ترجمة الربط بالكامل. هذا يعني أن مُترجم NativeAOT يملك الآن كل المعلومات لتقليم تطبيقك بشكل كامل وفعّال.

لتفعيل ترجمة الربط مع خاصية Source:

<PropertyGroup>
    <MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation>
</PropertyGroup>

تحسين أداء القوائم مع CollectionView

القوائم. هي في كل مكان في تطبيقات الموبايل، وهي أيضاً من أكثر العناصر تأثيراً على الأداء عند التعامل معها بشكل خاطئ. CollectionView هو البديل الأفضل لـ ListView القديم، مع دعم مدمج للمحاكاة الافتراضية (Virtualization).

قواعد ذهبية لأداء القوائم

أولاً: لا تضع CollectionView داخل ScrollView — أبداً

هذا خطأ شائع جداً وأراه يتكرر في مشاريع كثيرة. وضع CollectionView داخل ScrollView أو StackLayout يقتل آلية المحاكاة الافتراضية تماماً، ويجبر التطبيق على تحميل جميع العناصر دفعة واحدة في الذاكرة:

<!-- ❌ سيء: يكسر المحاكاة الافتراضية -->
<ScrollView>
    <StackLayout>
        <Label Text="عنوان" />
        <CollectionView ItemsSource="{Binding Items}" />
    </StackLayout>
</ScrollView>

<!-- ✅ صحيح: Grid يسمح بالمحاكاة الافتراضية -->
<Grid RowDefinitions="Auto,*">
    <Label Text="عنوان" Grid.Row="0" />
    <CollectionView ItemsSource="{Binding Items}"
                    Grid.Row="1" />
</Grid>

ثانياً: التحميل عند الطلب (Load on Demand)

من أكثر الأخطاء التي أصادفها: جلب جميع السجلات من الخادم دفعة واحدة. مع 50 سجلاً قد لا تلاحظ مشكلة، لكن مع 5000 سجل؟ التطبيق سيتجمد. الحل هو التحميل على دفعات:

public partial class ProductListViewModel : ObservableObject
{
    private readonly IProductService _productService;
    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 newProducts = await _productService
                .GetProductsAsync(_currentPage, PageSize);

            foreach (var product in newProducts)
            {
                Products.Add(product);
            }
            _currentPage++;
        }
        finally
        {
            IsLoadingMore = false;
        }
    }
}

وفي XAML، استخدم RemainingItemsThreshold لتفعيل التحميل التلقائي عند اقتراب المستخدم من نهاية القائمة:

<CollectionView ItemsSource="{Binding Products}"
                RemainingItemsThreshold="5"
                RemainingItemsThresholdReachedCommand="{Binding LoadMoreCommand}">
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="models:Product">
            <Grid Padding="12" ColumnDefinitions="60,*,Auto">
                <Image Source="{Binding ThumbnailUrl}"
                       WidthRequest="50"
                       HeightRequest="50"
                       Aspect="AspectFill" />
                <Label Text="{Binding Name}"
                       Grid.Column="1"
                       VerticalOptions="Center" />
                <Label Text="{Binding Price, StringFormat='{0:C}'}"
                       Grid.Column="2"
                       VerticalOptions="Center" />
            </Grid>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

البرمجة غير المتزامنة: لا تحبس الخيط الرئيسي

أي عملية تستغرق وقتاً — سواء جلب بيانات من API، معالجة صور، أو حفظ في قاعدة بيانات — يجب أن تكون غير متزامنة. حظر الخيط الرئيسي (UI Thread) يعني تجمد الواجهة بالكامل، وصدقني هذا أسوأ انطباع يمكن أن تتركه لدى المستخدم.

أنماط البرمجة غير المتزامنة الفعّالة

public partial class DataViewModel : ObservableObject
{
    [ObservableProperty]
    private bool _isBusy;

    [ObservableProperty]
    private string _statusMessage;

    // ✅ عملية غير متزامنة صحيحة
    [RelayCommand]
    private async Task ProcessDataAsync()
    {
        if (IsBusy) return;

        IsBusy = true;
        StatusMessage = "جارِ المعالجة...";

        try
        {
            // العمليات الثقيلة على خيط منفصل
            var result = await Task.Run(() =>
            {
                // معالجة حسابية مكثفة
                return HeavyComputation();
            });

            // تحديث الواجهة على الخيط الرئيسي
            StatusMessage = $"تم معالجة {result.Count} عنصراً";
        }
        catch (Exception ex)
        {
            StatusMessage = $"خطأ: {ex.Message}";
        }
        finally
        {
            IsBusy = false;
        }
    }
}

نمط الإقلاع الكسول (Lazy Startup)

نصيحة مهمة: لا تنتظر اكتمال العمليات غير المتزامنة أثناء إقلاع التطبيق. استخدم النمط الكسول لتأجيل التحميل حتى اللحظة التي تحتاج فيها البيانات فعلاً:

public class AppDataService
{
    private readonly Lazy<Task<AppConfig>> _configTask;

    public AppDataService(IConfigProvider provider)
    {
        // يبدأ التحميل عند أول وصول فقط
        _configTask = new Lazy<Task<AppConfig>>(
            () => provider.LoadConfigAsync());
    }

    public Task<AppConfig> GetConfigAsync() => _configTask.Value;
}

إدارة الذاكرة ومنع التسريبات

تسريبات الذاكرة في تطبيقات الموبايل مشكلة خبيثة. لن تلاحظها في البداية غالباً، لكن مع تنقل المستخدم بين الصفحات مراراً وتكراراً، يزداد استهلاك الذاكرة تدريجياً... حتى يتوقف التطبيق أو يُقتل من نظام التشغيل بلا رحمة.

الأسباب الشائعة لتسريب الذاكرة

1. الاشتراك في الأحداث بدون إلغاء الاشتراك

هذا هو المتهم الأول — يمثل حوالي 25% من مشاكل الموارد غير المُتتبعة. الحل بسيط: اشترك في OnAppearing وألغِ الاشتراك في OnDisappearing:

public partial class ProductPage : ContentPage
{
    private readonly INotificationService _notificationService;

    public ProductPage(INotificationService notificationService)
    {
        _notificationService = notificationService;
        InitializeComponent();
    }

    protected override void OnAppearing()
    {
        base.OnAppearing();
        // ✅ اشترك عند ظهور الصفحة
        _notificationService.ProductUpdated += OnProductUpdated;
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();
        // ✅ ألغِ الاشتراك عند اختفاء الصفحة
        _notificationService.ProductUpdated -= OnProductUpdated;
    }

    private void OnProductUpdated(object sender, ProductEventArgs e)
    {
        // التعامل مع التحديث
    }
}

2. Handler Disconnect: مشكلة خاصة بـ MAUI

هذه نقطة يغفل عنها كثيرون. في .NET MAUI، بعض عناصر التحكم (خاصةً تلك التي تتعامل مع موارد أصلية كمشغلات الفيديو) تحتاج تنظيفاً صريحاً عبر DisconnectHandler(). هذا ضروري بشكل خاص على منصات Apple حيث لا يتعامل جامع القمامة مع المراجع الدائرية:

public partial class VideoPlayerPage : ContentPage
{
    protected override void OnNavigatedFrom(NavigatedFromEventArgs args)
    {
        base.OnNavigatedFrom(args);

        // تنظيف Handler عند مغادرة الصفحة
        var handler = videoPlayer.Handler;
        if (handler != null)
        {
            handler.DisconnectHandler();
        }
    }
}

3. WeakEventManager للناشرين طويلي العمر

إذا كان لديك كائن يعيش طوال عمر التطبيق ويُطلق أحداثاً (مثل خدمة Singleton)، استخدم WeakEventManager لمنع تسريب المشتركين:

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

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

    public void Publish(string message)
    {
        _eventManager.HandleEvent(
            this,
            new MessageEventArgs(message),
            nameof(MessageReceived));
    }
}

MemoryToolkit.Maui: أداة لا غنى عنها لكشف التسريبات

مكتبة MemoryToolkit.Maui أداة ممتازة لاكتشاف تسريبات الذاكرة وتشخيصها. أنصح بإعدادها في كل مشروع MAUI جديد:

// في MauiProgram.cs
public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder.UseMauiApp<App>();

#if DEBUG
    builder.Logging.AddDebug();
    // تفعيل كشف التسريبات في وضع التطوير فقط
    builder.UseLeakDetection(collectionTarget =>
    {
        // يتم تنفيذ هذا الكود عند اكتشاف تسريب
        Debug.WriteLine(
            $"⚠️ تسريب ذاكرة مُكتشف: {collectionTarget}");
    });
#endif

    return builder.Build();
}

المكتبة توفر TearDownBehavior الذي يعمل على ثلاث مراحل: تنظيف BindingContext، فصل المراجع بين العناصر، ثم استدعاء DisconnectHandler() و Dispose(). يعني باختصار، تريحك من الكثير من العمل اليدوي.

تحسين حقن التبعيات (Dependency Injection)

حقن التبعيات في .NET MAUI أداة قوية، لكنها سلاح ذو حدين. استخدامها بدون حكمة يمكن أن يتحول لمصدر مشاكل أداء بدلاً من حل.

قواعد لتحسين DI

1. سجّل فقط ما تحتاجه

public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();

    // ✅ سجّل الخدمات حسب الحاجة
    builder.Services.AddSingleton<IApiClient, ApiClient>();
    builder.Services.AddSingleton<ICacheService, CacheService>();

    // ✅ الصفحات كـ Transient للتحرر بعد الاستخدام
    builder.Services.AddTransient<ProductsPage>();
    builder.Services.AddTransient<ProductsViewModel>();

    return builder.Build();
}

2. استخدم Lazy<T> للخدمات الثقيلة

لماذا تُنشئ خدمة ثقيلة عند إقلاع التطبيق إذا كان المستخدم قد لا يحتاجها أصلاً؟

public class AnalyticsViewModel
{
    // لا يتم إنشاء الخدمة حتى الوصول الفعلي
    private readonly Lazy<IReportingService> _reportingService;

    public AnalyticsViewModel(
        Lazy<IReportingService> reportingService)
    {
        _reportingService = reportingService;
    }

    [RelayCommand]
    private async Task GenerateReportAsync()
    {
        // هنا فقط يتم إنشاء الخدمة
        var report = await _reportingService.Value
            .GenerateAsync();
    }
}

3. تجنب IDisposable على الخدمات المؤقتة

نقطة يقع فيها كثيرون: الخدمات المؤقتة (Transient) التي تنفذ IDisposable لا يتم التخلص منها تلقائياً. استخدم دالة تنظيف صريحة بدلاً من ذلك:

// ❌ تجنب هذا
public class DataProcessor : IDisposable
{
    public void Dispose() { /* ... */ }
}

// ✅ استخدم هذا بدلاً
public class DataProcessor
{
    private Timer _timer;

    public void CleanUp()
    {
        _timer?.Change(Timeout.Infinite, 0);
        _timer?.Dispose();
        _timer = null;
    }
}

تحسين الصور: التفصيلة التي تصنع الفرق

الصور من أكبر مستهلكي الذاكرة في تطبيقات الموبايل، وكثير من المطورين يتجاهلون هذه النقطة. صورة واحدة بدقة عالية قد تستهلك عشرات الميغابايت من الذاكرة عند فك ترميزها — حتى لو كان حجم الملف صغيراً.

استراتيجيات تحسين الصور

<!-- ✅ حدد أبعاد العرض لتقليل استهلاك الذاكرة -->
<Image Source="{Binding ImageUrl}"
       WidthRequest="100"
       HeightRequest="100"
       Aspect="AspectFill" />

<!-- ❌ تجنب ترك الصورة بدون أبعاد محددة -->
<Image Source="{Binding FullResImageUrl}" />

النقاط الأساسية:

  • استخدم صور بالحجم المناسب — لا تحمّل صورة 4000x3000 لعرضها في مربع 100x100 (يبدو بديهياً لكنه يحصل كثيراً)
  • حرّر الصور عند عدم الحاجة لها، خاصةً في Page.Disappearing
  • فعّل التخزين المؤقت (Caching) لتجنب إعادة تحميل نفس الصور
  • استخدم تنسيقات فعّالة مثل WebP بدلاً من PNG حيثما أمكن

استخدام Shell للتنقل عند الطلب

من أهم مزايا Shell في .NET MAUI أنها تنشئ الصفحات عند الطلب فقط وليس عند إقلاع التطبيق. هذا يعني أن التطبيق لا يحمّل جميع الصفحات في الذاكرة دفعة واحدة — وهذا فرق كبير في تطبيق يحتوي على عشرات الصفحات:

<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:views="clr-namespace:MyApp.Views">

    <!-- الصفحات تُنشأ فقط عند التنقل إليها -->
    <TabBar>
        <ShellContent Title="الرئيسية"
                      ContentTemplate="{DataTemplate views:HomePage}" />
        <ShellContent Title="المنتجات"
                      ContentTemplate="{DataTemplate views:ProductsPage}" />
        <ShellContent Title="الإعدادات"
                      ContentTemplate="{DataTemplate views:SettingsPage}" />
    </TabBar>

    <!-- التنقل عبر المسارات -->
    <Shell.FlyoutContent>
        <!-- ... -->
    </Shell.FlyoutContent>
</Shell>

السر هنا في استخدام ContentTemplate بدلاً من Content — هذا ما يؤجل إنشاء الصفحة حتى يتنقل المستخدم إليها فعلاً. تفصيلة صغيرة لكن أثرها كبير.

أدوات التوصيف (Profiling): قِس ثم حسّن

القاعدة الذهبية التي أكررها دائماً: لا تحسّن بناءً على التخمين. استخدم أدوات التوصيف لتحديد الاختناقات الفعلية — قد تتفاجأ أن المشكلة ليست أبداً حيث تتوقعها.

dotnet-trace: التوصيف عبر المنصات

أداة dotnet-trace تعمل على جميع المنصات وتوفر نظرة تفصيلية على أداء التطبيق:

# تثبيت الأداة
dotnet tool install --global dotnet-trace

# بدء جلسة توصيف
dotnet-trace collect --process-id <PID> --providers Microsoft-DotNETRuntimeMonoProfiler

# لتطبيقات Android عبر dsrouter
dotnet-dsrouter android

dotnet-gcdump: تحليل الذاكرة

لتحليل استخدام الذاكرة وكشف التسريبات بشكل دقيق:

# تثبيت الأداة
dotnet tool install --global dotnet-gcdump

# جمع لقطة ذاكرة
dotnet-gcdump collect --process-id <PID>

# النتيجة ملف .gcdump يمكن فتحه في Visual Studio أو PerfView

نصائح عملية للتوصيف

  • اختبر على أجهزة حقيقية: المحاكيات لا تعكس الأداء الفعلي أبداً
  • اختبر على أجهزة متوسطة ومنخفضة: ليس كل المستخدمين يملكون أحدث الهواتف
  • قارن Debug و Release: أداء Android في Debug أبطأ بكثير — لا تحكم بناءً عليه
  • جرّب DotNet.Meteor: إضافة VS Code ممتازة لتشغيل وتوصيف تطبيقات MAUI

قائمة مرجعية سريعة قبل النشر

قبل نشر أي تطبيق .NET MAUI، مرّ على هذه القائمة:

  • الربط: جميع الربطات مُجمَّعة باستخدام x:DataType
  • التخطيط: لا تداخل أكثر من 3 مستويات
  • القوائم: CollectionView داخل Grid وليس ScrollView
  • البيانات: تحميل عند الطلب للقوائم الطويلة
  • الصور: أبعاد محددة وحجم مناسب
  • الأحداث: إلغاء الاشتراك في OnDisappearing
  • AOT: مفعّل للمنصات المدعومة
  • DI: كل خدمة مسجلة بالنوع المناسب
  • الخيوط: العمليات الثقيلة على Task.Run
  • التنقل: Shell مع ContentTemplate
  • التوصيف: اختبار فعلي على أجهزة متوسطة المواصفات

الخلاصة

تحسين أداء تطبيقات .NET MAUI ليس مهمة تنجزها مرة وتنساها. هو عملية مستمرة تبدأ من اللحظة الأولى للتطوير وتستمر طوال عمر المشروع. لكن الخبر السار أن معظم التحسينات التي ذكرناها هنا سهلة التطبيق ولا تحتاج تغييرات جذرية.

ابدأ بالربط المُجمَّع — هو أسهل تحسين وتأثيره فوري. ثم انتقل لتسطيح الواجهة وتحسين القوائم. بعدها فعّل Native AOT واستخدم أدوات التوصيف لاصطياد أي اختناقات متبقية.

وتذكّر: قِس أولاً، ثم حسّن. لا تعتمد على حدسك — دع الأرقام والبيانات ترشدك. واختبر دائماً على أجهزة حقيقية بمواصفات متنوعة، لأن المحاكي لن يخبرك بالقصة الكاملة أبداً.

بتطبيق هذه الممارسات، ستبني تطبيقات .NET MAUI سريعة وسلسة واقتصادية في استهلاك الموارد — وفي النهاية، هذا بالضبط ما يتوقعه مستخدموك منك.

عن الكاتب Editorial Team

Our team of expert writers and editors.