بهروزرسانی: ۲ ژوئن ۲۰۲۶
بهینهسازی عملکرد در .NET MAUI یعنی کاهش زمان راهاندازی اپلیکیشن (Cold Startup) از طریق فعالسازی Profiled AOT و NativeAOT روی iOS، کاهش مصرف حافظه با حذف نشتهای مرتبط با Event Handlerها و WeakReference، و افزایش روانی UI با Virtualization صحیح CollectionView و کشگذاری تصاویر. در نسخه .NET 9 و مسیر .NET 10، مایکروسافت ابزارهای رسمی مانند dotnet-trace، dotnet-gcdump و پروفایلرهای داخلی MauiProgram را برای اندازهگیری دقیق این بهبودها ارائه کرده است.
چرا اپلیکیشن .NET MAUI من کند بالا میآید؟
راستش را بخواهید، سه دلیل اصلی برای Cold Startup کند در .NET MAUI وجود دارد: کامپایل JIT متدهای Bootstrap هنگام راهاندازی، رجیستر کردن تعداد زیاد Handler و Service در MauiProgram.cs، و بارگذاری مجموعههای (Assembly) غیرضروری بهخاطر غیرفعال بودن Linker. وقتی Profiled AOT خاموش باشد، CLR در هر اجرا باید متدهای Critical Path را کامپایل کند که در دستگاههای میانرده اندروید (مثلاً Samsung A14) بهسادگی ۱.۵ تا ۳ ثانیه به TTFD (Time To First Draw) اضافه میکند.
در تجربه ما، اولین قدم همیشه اندازهگیری دقیق است نه حدس. با فعالسازی MAUI_PERF_LOG=1 در متغیرهای محیطی، MAUI خودش لاگهای مرحلهبهمرحله Bootstrap را در Logcat (اندروید) یا Console (iOS) چاپ میکند. اگر مدت زمان مرحله RegisterServices بالاست، تعداد سرویسهای تزریقشده را بازنگری کنید؛ اگر BuildHandlers کند است، Handlerهای کاستوم را Lazy رجیستر کنید. این الگو با همان منطقی که در معماری MVVM در .NET MAUI توضیح دادیم سازگار است: تزریق وابستگی باید سبک و Lazy باشد.
Profiled AOT و کاهش زمان Startup در اندروید
Profiled AOT یک Optimization مخصوص اندروید است که بر اساس یک پروفایل از پیش جمعآوریشده مشخص میکند کدام متدها در Critical Path اجرا قرار دارند و فقط آنها را AOT کامپایل میکند. این رویکرد بین حجم باینری و سرعت Startup تعادل برقرار میکند. فعالسازی آن در فایل .csproj ساده است:
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)' == 'Release|net9.0-android'">
<RunAOTCompilation>true</RunAOTCompilation>
<AndroidEnableProfiledAot>true</AndroidEnableProfiledAot>
<AndroidStripILAfterAOT>true</AndroidStripILAfterAOT>
<AndroidLinkMode>SdkOnly</AndroidLinkMode>
<EnableLLVM>true</EnableLLVM>
</PropertyGroup>تنظیم AndroidStripILAfterAOT به true بایتکد IL را پس از کامپایل AOT حذف میکند. در نتیجه هم حجم APK کم میشود و هم زمان بارگذاری Assemblyها در RAM کاهش مییابد. EnableLLVM از کامپایلر LLVM بهجای Mono mini استفاده میکند که در Benchmarkهای رسمی مایکروسافت روی Pixel 7 حدود ۱۸٪ سریعتر است. برای آپلیکیشنهای بزرگتر میتوانید پروفایل سفارشی تولید کنید: ابتدا با AndroidGenerateAotProfile=true برنامه را در یک مسیر متداول اجرا کنید (مثلاً ورود کاربر و باز شدن داشبورد)، فایل custom.aprof تولید میشود و سپس آن را در AndroidAotProfile Item Group اضافه کنید.
طبق مستندات رسمی Performance .NET MAUI، ترکیب Profiled AOT + LLVM + StartupTracing روی دستگاههای اندروید Cold Startup را به طور میانگین از ۲.۸s به ۱.۶s کاهش میدهد. در پروژه قبلی، با همین ترکیب توانستیم زمان شروع یک اپ خردهفروشی را از حدود ۳ ثانیه به زیر ۱.۷ ثانیه برسانیم. برای اپلیکیشنهایی که اتصال به REST API سنگین در Startup دارند، حتماً فراخوانیهای شبکه را به OnAppearing یا یک Background Service منتقل کنید نه به App.xaml.cs.
NativeAOT در iOS: وضعیت و فعالسازی
پشتیبانی NativeAOT برای iOS از .NET 9 GA شد و در .NET 10 (پیشنمایش بهار ۲۰۲۶) بهعنوان مسیر پیشفرض برای انتشار در App Store توصیه میشود. مزیت اصلی NativeAOT حذف کامل Runtime مدیریتشده و تولید یک باینری Mach-O واقعی است. نتیجه؟ زمان Startup در iPhone 13 تا ۲ برابر سریعتر و حجم IPA تا ۲.۵ برابر کوچکتر. برای فعالسازی، در .csproj اضافه کنید:
<PropertyGroup Condition="$(TargetFramework.Contains('-ios'))">
<PublishAot>true</PublishAot>
<_IsPublishing>true</_IsPublishing>
<StripSymbols>true</StripSymbols>
<IlcOptimizationPreference>Speed</IlcOptimizationPreference>
</PropertyGroup>برای اطمینان از سازگاری، Warningهای IL2026 و IL3050 در Build لاگ را جدی بگیرید؛ این هشدارها دقیقاً مشخص میکنند کدام متد در Runtime به مشکل میخورد. ابزار dotnet publish -f net9.0-ios -p:PublishAot=true یک گزارش کامل از Trimming و AOT تولید میکند. در یک پروژه Production که اخیراً مهاجرت دادیم، با NativeAOT حجم IPA از ۸۲MB به ۳۱MB کاهش پیدا کرد. به نظر من بزرگترین برد NativeAOT همین کاهش اندازه است که مستقیماً نرخ نصب از App Store را بالاتر میبرد.
چگونه مصرف حافظه را در .NET MAUI کاهش دهیم؟
نشت حافظه (Memory Leak) در MAUI اغلب نه از کدهای Native بلکه از سه الگوی رایج C# نشأت میگیرد: ثبت Event Handler بدون Unsubscribe، استفاده از WeakReferenceMessenger بهاشتباه بهصورت Strong، و نگهداری Page در یک Static Field. این موارد باعث میشوند Garbage Collector نتواند Page یا ViewModel را پاک کند و در هر بار Navigate یک نمونه جدید روی Heap باقی بماند.
الگوی استاندارد برای اشتراک ایمن در رویدادها این است:
public partial class ProductsPage : ContentPage
{
private readonly IConnectivity _connectivity;
public ProductsPage(IConnectivity connectivity)
{
InitializeComponent();
_connectivity = connectivity;
}
protected override void OnAppearing()
{
base.OnAppearing();
_connectivity.ConnectivityChanged += OnConnectivityChanged;
}
protected override void OnDisappearing()
{
base.OnDisappearing();
_connectivity.ConnectivityChanged -= OnConnectivityChanged;
}
private void OnConnectivityChanged(object? sender, ConnectivityChangedEventArgs e)
{
// Handle change
}
}برای پیامرسانی بین ViewModelها از WeakReferenceMessenger.Default در CommunityToolkit.Mvvm استفاده کنید نه MessagingCenter قدیمی که در .NET 8 Deprecated شد و در .NET 10 کاملاً حذف میشود. این الگو با همان معماری معماری Offline-First در .NET MAUI هماهنگ است و از نشت در Sync Service جلوگیری میکند.
برای تشخیص نشتها از dotnet-gcdump استفاده کنید:
dotnet-gcdump collect -p <pid>
dotnet-gcdump report mauiapp.gcdumpابزار dotnet-gcdump رسمی مایکروسافت نمای کامل از Object Graph میدهد و دقیقاً نشان میدهد کدام Page یا ViewModel هنوز Reference دارد. الگوی رایج این است که Navigate به ۱۰ صفحه و برگشت به Root باید ۱۰ نمونه آزاد شود. اگر آزاد نشدند، Leak دارید.
بهینهسازی CollectionView و اسکرول روان
کنترل CollectionView در MAUI جایگزین مدرن ListView است و Virtualization بومی روی RecyclerView (اندروید) و UICollectionView (iOS) دارد. اما تنظیمات پیشفرض همیشه بهینه نیستند. برای لیستهای بزرگ این Propertyها را تنظیم کنید:
<CollectionView
ItemsSource="{Binding Products}"
ItemsUpdatingScrollMode="KeepItemsInView"
RemainingItemsThreshold="10"
RemainingItemsThresholdReachedCommand="{Binding LoadMoreCommand}"
ItemSizingStrategy="MeasureFirstItem">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical" ItemSpacing="8" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Product">
<Grid Padding="12" ColumnDefinitions="60,*">
<Image Source="{Binding ThumbnailUrl}"
Aspect="AspectFill"
HeightRequest="60"
WidthRequest="60" />
<Label Grid.Column="1"
Text="{Binding Name}"
VerticalOptions="Center" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>نکته کلیدی ItemSizingStrategy="MeasureFirstItem" است؛ بهجای اندازهگیری همه آیتمها، MAUI فقط اولین آیتم را اندازه میگیرد و فرض میکند بقیه هماندازهاند. این تنظیم برای لیستهای یکسان (مثل یک Feed محصول) سرعت Layout را تا ۵ برابر افزایش میدهد. علاوه بر این، استفاده از x:DataType Compiled Bindings را فعال میکند که از Reflection اجتناب کرده و در Benchmarkهای رسمی MAUI تا ۸ برابر سریعتر از Binding پویاست.
برای Paging از RemainingItemsThreshold بهجای event Scrolled استفاده کنید. این الگو مصرف CPU را در Main Thread پایین نگه میدارد. اگر آیتمهای شما اندازههای مختلف دارند، ItemSizingStrategy="Measured" ضروری است، اما حتماً تعداد آیتمهای همزمان روی صفحه را با Pagination محدود کنید.
مدیریت تصاویر و Downsampling
تصاویر یکی از منابع اصلی مصرف حافظه در اپلیکیشنهای موبایل هستند. یک تصویر ۴K (۳۸۴۰×۲۱۶۰) در حافظه تقریباً ۳۳MB اشغال میکند، حتی اگر در یک Image ۱۰۰×۱۰۰ نمایش داده شود. MAUI بهصورت پیشفرض تصاویر را Cache میکند اما Downsampling خودکار ندارد. راهحل، استفاده از UriImageSource با CacheValidity یا کتابخانههای Successor مانند Sharpnado.CollectionView است.
<Image x:Name="hero">
<Image.Source>
<UriImageSource
Uri="https://cdn.example.com/banner.jpg"
CacheValidity="7"
CachingEnabled="True" />
</Image.Source>
</Image>تنظیم CacheValidity بر حسب روز مشخص میکند تصویر چقدر در Disk Cache نگهداری شود. برای تصاویر بزرگ، حتماً سرور را بهگونهای تنظیم کنید که نسخههای با اندازههای مختلف ارائه دهد (Responsive Images) و در کلاینت بر اساس DeviceDisplay.MainDisplayInfo.Density URL مناسب را انتخاب کنید. این الگو با چیزی که در ذخیرهسازی محلی با SQLite برای Caching دادهها داشتیم همخوانی دارد.
Linker، Trimming و کاهش حجم باینری
Linker (که در .NET به آن IL Linker یا illink میگویند) کدهای استفادهنشده را از Output حذف میکند. برای MAUI سه حالت اصلی وجود دارد: None (هیچ حذفی)، SdkOnly (فقط BCL)، و Full (همه چیز شامل کد شما). حالت Full تهاجمیترین است و حجم APK را تا ۶۰٪ کاهش میدهد اما با Reflection ناسازگاری دارد.
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
</PropertyGroup>برای حفظ کلاسهایی که Reflection روی آنها انجام میشود (مثلاً ViewModelها در Binding یا کلاسهای Serialization)، از Attribute DynamicallyAccessedMembers یا DynamicDependency استفاده کنید:
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(Product))]
public static class TrimmerHints { }طبق مستندات رسمی Trimming Options در .NET، فهرست کاملی از Attributeها و بهترین رویکردها در دسترس است. توصیه من این است که در ابتدا با SdkOnly شروع کنید، با اطمینان از کار کردن همه قابلیتها به partial ارتقا دهید و فقط در صورت نیاز به Full بروید. این رویکرد ریسک Crashهای Production را بهشدت کم میکند.
اندازهگیری و پروفایلینگ با ابزارهای رسمی
بدون اندازهگیری، بهینهسازی فقط حدس است. ابزارهای پیشنهادی برای پروفایلینگ MAUI در ۲۰۲۶:
| ابزار | کاربرد | پلتفرم | نصب |
|---|---|---|---|
| dotnet-trace | CPU، JIT، GC Events | iOS + Android | dotnet tool install |
| dotnet-gcdump | Heap snapshot و نشت حافظه | iOS + Android | dotnet tool install |
| PerfView | تحلیل عمیق .nettrace | Windows | GitHub Release |
| Xcode Instruments | Profile رفتار Native iOS | iOS | Mac App Store |
| Android Studio Profiler | CPU، Memory، Network اندروید | Android | Android Studio |
| MAUI Startup Tracing | Timeline مرحلهای Bootstrap | iOS + Android | MAUI_PERF_LOG=1 |
یک Workflow پیشنهادی: ابتدا با MAUI_PERF_LOG=1 منشأ کندی Startup را پیدا کنید، سپس با dotnet-trace فایل .nettrace بسازید و در PerfView باز کنید. سربرگ JITStats نشان میدهد چه مقدار زمان صرف JIT شده. اگر بالای ۲۰۰ms است، Profiled AOT را فعال کنید. برای نشت حافظه، در دو نقطه زمانی dotnet-gcdump collect بگیرید و در PerfView Diff کنید تا اشیاء جدید باقیمانده مشخص شوند.
چکلیست نهایی Production-Ready
قبل از انتشار در App Store یا Google Play، این لیست را مرور کنید تا مطمئن شوید همه بهینهسازیهای کلیدی اعمال شدهاند:
RunAOTCompilationوAndroidEnableProfiledAotدر Release فعال است.- روی iOS از NativeAOT یا حداقل
UseInterpreter=falseاستفاده شده است. - همه Pageها در
OnDisappearingمنابع و Eventها را Unsubscribe میکنند. - همه
CollectionViewها دارایx:DataTypeوItemSizingStrategyمناسب هستند. - تصاویر بزرگ Downsample میشوند و
CacheValidityتنظیم شده است. PublishTrimmed=trueباTrimMode=fullیاpartialفعال است و Warningهای IL2xxx برطرف شدهاند.- Cold Startup روی یک دستگاه میانرده اندازهگیری شده و کمتر از ۲ ثانیه است.
- یک Round-Trip Navigate به ۱۰ صفحه و برگشت، حافظه را به سطح اولیه (±۵٪) برمیگرداند.
- حجم APK کمتر از ۴۰MB و IPA کمتر از ۵۰MB است.
صادقانه بگویم، بهترین کار این است که این چکلیست را در CI/CD بهصورت Smoke Test پیادهسازی کنید: یک Job که Cold Startup را با adb shell am start -W اندازه میگیرد و در صورت تجاوز از Threshold، Pipeline را Fail میکند. این Discipline در بلندمدت کیفیت اپ شما را در ردیف برترینهای فروشگاه نگه میدارد.
پرسشهای متداول
آیا NativeAOT در .NET MAUI آماده Production است؟
بله، از .NET 9 پشتیبانی NativeAOT برای iOS بهصورت Stable در دسترس است و در .NET 10 برای اندروید نیز در حال پیشنمایش است. شرکتهای بزرگی مانند Microsoft و Stack Overflow اپلیکیشنهای Production خود را با NativeAOT منتشر کردهاند، اما باید کتابخانههای مورد استفاده با Trimming و Reflection محدود سازگار باشند.
CollectionView بهتر است یا ListView؟
قطعاً CollectionView. کنترل ListView در MAUI بهخاطر سازگاری با Xamarin.Forms حفظ شده اما Performance و قابلیتهای کمتری دارد. CollectionView از Virtualization بومی پلتفرم استفاده میکند، Layoutهای انعطافپذیرتر دارد و در Benchmarkهای رسمی تا ۳ برابر سریعتر اسکرول میکند.
چرا برنامه من بعد از مدتی کند میشود؟
این علامت کلاسیک نشت حافظه است. معمولاً Event Handlerهای Unsubscribe-نشده، Static Listهایی که رشد میکنند، یا Image Cache بدون محدودیت اندازه دلیل آن هستند. با dotnet-gcdump دو Snapshot با فاصله بگیرید و Diff کنید تا منبع نشت پیدا شود.
آیا فعالسازی Full Trimming خطرناک است؟
اگر تستهای جامع داشته باشید، نه. Full Trimming حجم را تا ۶۰٪ کاهش میدهد اما کدهایی که با Reflection فراخوانی میشوند ممکن است حذف شوند. توصیه ما این است که با SdkOnly شروع کنید، تمام Warningهای IL2xxx را برطرف کنید و سپس به Full ارتقا دهید.
چطور زمان Startup را در CI اندازهگیری کنیم؟
روی اندروید از adb shell am start -W -n com.your.app/.MainActivity استفاده کنید که زمان TotalTime و WaitTime را برمیگرداند. روی iOS از Xcode XCTest با measure block و XCTApplicationLaunchMetric استفاده کنید. این مقادیر را در یک فایل JSON ذخیره کنید و در Pipeline با مقدار مرجع مقایسه کنید.

