Native AOT у .NET MAUI 10: як зменшити розмір додатка вдвічі та прискорити запуск

Дізнайтеся, як Native AOT у .NET MAUI 10 зменшує розмір додатка до 2x та прискорює запуск на iOS, Mac Catalyst та Android. Покрокова конфігурація, бенчмарки, вирішення проблем тримінгу та AOT-сумісні патерни коду.

Навіщо мобільному додатку Native AOT

Уявіть: ваш .NET MAUI додаток важить 80 МБ і запускається три секунди. Користувач тим часом вже відкрив конкурента на Swift, зробив те, що хотів, і закрив. Знайомо? Ось саме тут Native AOT може серйозно змінити гру.

Native AOT — це компіляція вашого C# коду безпосередньо в машинний код цільової платформи, без рантайму Mono. Простіше кажучи, додаток стає ближчим до нативного за розміром і швидкістю запуску.

У .NET 10 (листопад 2025) Native AOT для iOS та Mac Catalyst став повністю продакшн-готовим. Android — поки на фінальній стадії експериментальної розробки, але результати вже вражають. Давайте розберемо все по поличках: як увімкнути, що це дає в реальних цифрах, і які підводні камені чекають на шляху.

Як працює Native AOT: основи для мобільного розробника

Стандартний процес виконання .NET MAUI додатка виглядає так: компілятор перетворює C# код у проміжний байт-код (IL), а рантайм Mono інтерпретує або JIT-компілює його вже під час роботи програми. Гнучко? Так. Але за цю гнучкість ви платите розміром (потрібен увесь рантайм) і часом запуску (JIT-компіляція не безкоштовна).

Native AOT працює принципово інакше.

Під час публікації (dotnet publish) весь код аналізується статично, невикористані частини агресивно вирізаються (trimming), а те, що залишилось, компілюється прямо в нативний машинний код. На виході — самодостатній бінарник без жодної залежності від рантайму.

Що відбувається під капотом

  • Статичний аналіз — компілятор будує повний граф залежностей і визначає, який код реально використовується
  • Full trimming — все зайве видаляється, включаючи частини стандартної бібліотеки .NET, які вам не потрібні
  • AOT-компіляція — IL перетворюється в нативний машинний код під конкретну архітектуру (arm64 для iOS, x64 для Mac Catalyst)
  • Статичне лінкування — всі залежності вбудовуються безпосередньо в один бінарний файл

Важливий нюанс: Native AOT працює лише під час dotnet publish. Коли ви тиснете F5 у Visual Studio або запускаєте dotnet build, додаток використовує звичайний Mono. І це зроблено навмисно — під час розробки швидкість збірки важливіша за розмір бінарника.

Реальні бенчмарки: що дає Native AOT у цифрах

Теорія — це добре, але давайте подивимось на конкретні числа. Бо, чесно кажучи, саме вони вирішують.

iOS та Mac Catalyst (продакшн-готовий)

МетрикаMono (за замовчуванням)Native AOTПокращення
Розмір додатка (iOS)~80 МБ~40 МБдо 2x менше
Розмір додатка (Mac Catalyst)~90 МБ~45 МБдо 2x менше
Час запуску (iOS)~1200 мс~600 мсдо 2x швидше
Час запуску (Mac Catalyst)~500 мс~420 мс~1.2x швидше
Час збіркиБазовийШвидшеПомітне покращення

На iOS різниця найвідчутніша — вдвічі менший розмір і вдвічі швидший старт. На Mac Catalyst виграш у швидкості запуску скромніший (бо там і без AOT все стартує досить швидко), але зменшення розміру все одно суттєве.

Android (експериментальний у .NET 10)

А ось тут стає по-справжньому цікаво. Тестування на Samsung Galaxy Note 10+ з .NET 10 RC2 показало такі результати:

МетрикаMonoAOTNative AOTПокращення
Час запуску (з ініціалізацією)1270–1410 мс271–331 мсдо 4x швидше

Чотирикратне прискорення запуску — це не друкарська помилка. Але (і це важливе «але») Native AOT на Android поки що має статус експериментального. Для продакшну доведеться почекати до .NET 11.

Увімкнення Native AOT для iOS та Mac Catalyst

Отже, переходимо до практики. Процес увімкнення Native AOT насправді нескладний, але є кілька моментів, де легко помилитися.

Крок 1: Підготовка середовища

Переконайтеся, що у вас встановлено:

  • .NET 10 SDK (або новіша версія)
  • Xcode 16+ (для iOS та Mac Catalyst)
  • Visual Studio 2026 або VS Code з розширенням .NET MAUI
# Перевірити версію SDK
dotnet --version

# Переконатися, що MAUI workload встановлений
dotnet workload list | grep maui

Крок 2: Увімкнення Native AOT у файлі проєкту

Додайте властивість PublishAot до вашого .csproj:

<PropertyGroup>
  <!-- Увімкнути Native AOT для iOS та Mac Catalyst -->
  <PublishAot>true</PublishAot>
</PropertyGroup>

Хочете увімкнути тільки для конкретних платформ? Використовуйте умови:

<PropertyGroup Condition="$(TargetFramework.Contains('-ios')) Or $(TargetFramework.Contains('-maccatalyst'))">
  <PublishAot>true</PublishAot>
</PropertyGroup>

Крок 3: Публікація додатка

Тепер запускаємо публікацію:

# Публікація для iOS (пристрій)
dotnet publish -f net10.0-ios -c Release -r ios-arm64

# Публікація для iOS Simulator
dotnet publish -f net10.0-ios -c Release -r iossimulator-arm64

# Публікація для Mac Catalyst
dotnet publish -f net10.0-maccatalyst -c Release -r maccatalyst-arm64

# Запуск на пристрої після публікації
dotnet publish -f net10.0-ios -c Release -r ios-arm64 /t:Run

Важливо: не встановлюйте TrimMode вручну при використанні Native AOT. Він автоматично застосовує повне обрізання — додаткові налаштування тут зайві й можуть навіть нашкодити.

Оптимізація AOT для Android: що можна зробити вже зараз

Повноцінний Native AOT для Android ще експериментальний, але це не означає, що вам нічого не залишається. .NET 10 дає доволі потужні інструменти оптимізації, які вже готові для продакшну.

Profiled AOT + IL Stripping

Найефективніша комбінація — профільована AOT-компіляція плюс видалення IL-коду. По суті, це як полегшена версія Native AOT:

<PropertyGroup Condition="$(TargetFramework.Contains('-android'))">
  <!-- Видаляти IL-код після AOT-компіляції -->
  <AndroidStripILAfterAOT>true</AndroidStripILAfterAOT>
  
  <!-- Поєднати з профільованою AOT для оптимального результату -->
  <AndroidEnableProfiledAot>true</AndroidEnableProfiledAot>
  
  <!-- Увімкнути R8 для додаткового зменшення розміру -->
  <AndroidLinkTool>r8</AndroidLinkTool>
</PropertyGroup>

Як це працює: спочатку обрані методи (ті, що реально використовуються під час типового сценарію роботи) компілюються в нативний код через AOT. Потім оригінальний IL для цих методів видаляється з фінальної збірки. Результат — менший APK без втрати продуктивності.

Експериментальний Native AOT на Android

Якщо хочете спробувати повноцінний Native AOT на Android (тільки для тестування, не для продакшну!):

<PropertyGroup Condition="$(TargetFramework.Contains('-android'))">
  <PublishAot>true</PublishAot>
</PropertyGroup>
# Публікація з Native AOT для Android
dotnet publish -f net10.0-android -c Release

Зверніть увагу: при публікації ви побачите попередження IL3053Assembly 'Mono.Android' produced AOT analysis warnings. Це нормально і означає, що частини Android-рантайму ще не повністю адаптовані під Native AOT. Просто не використовуйте це для збірок, які йдуть до користувачів.

Вирішення проблем сумісності з тримінгом та AOT

Ось де починається найцікавіше (і найболючіше). Найбільший виклик при впровадженні Native AOT — сумісність вашого коду та сторонніх бібліотек з тримінгом.

Золоте правило: якщо додаток публікується без попереджень тримера — він працюватиме коректно після AOT-компіляції. Попередження — ваш головний маркер проблем.

Типові джерела попереджень

У більшості випадків проблеми виникають через рефлексію та динамічний код:

// ❌ Проблемний код — рефлексія без анотацій
public object CreateService(string typeName)
{
    var type = Type.GetType(typeName);        // IL2057
    return Activator.CreateInstance(type);     // IL2067
}

// ✅ AOT-сумісний варіант — використання дженериків
public T CreateService<T>() where T : new()
{
    return new T();
}

// ✅ Або використання DI-контейнера
services.AddTransient<IMyService, MyService>();

Робота з невідомими типами

Потрібна динамічна десеріалізація? Забудьте про рефлексію — використовуйте Source Generators:

// ❌ Проблемний код — System.Text.Json з рефлексією
var result = JsonSerializer.Deserialize<MyModel>(json);

// ✅ AOT-сумісний варіант — Source Generator
[JsonSerializable(typeof(MyModel))]
[JsonSerializable(typeof(List<MyModel>))]
internal partial class AppJsonContext : JsonSerializerContext { }

// Використання
var result = JsonSerializer.Deserialize(json, AppJsonContext.Default.MyModel);

Перевірка сумісності сторонніх бібліотек

Чесно кажучи, єдиний надійний спосіб перевірити, чи працює бібліотека з AOT, — опублікувати додаток і подивитися на попередження:

# Публікація з детальним логуванням попереджень
dotnet publish -f net10.0-ios -c Release -r ios-arm64 -v detailed 2>&1 | grep -E "IL[0-9]{4}|warning"

Ось як виглядає ситуація із сумісністю популярних бібліотек станом на 2026 рік:

БібліотекаAOT-сумісністьПримітки
MVVM Community Toolkit✅ ПовнаSource Generators за замовчуванням
System.Text.Json✅ ПовнаПотребує JsonSerializerContext
SQLite-net⚠️ ЧастковаУникайте рефлексивних запитів
Entity Framework Core⚠️ ЧастковаCompiled Queries обов'язкові
Newtonsoft.Json❌ НіЗамініть на System.Text.Json
Azure SDK✅ ЗростаєБільшість пакетів оновлено
Syncfusion/Telerik⚠️ ЧастковаПеревіряйте конкретні компоненти

Як бачите, Newtonsoft.Json — це перше, від чого доведеться відмовитись. Якщо ваш проєкт все ще використовує його — міграція на System.Text.Json стає не просто бажаною, а обов'язковою.

Feature Switches для MAUI

.NET MAUI має вбудовані перемикачі (feature switches), що дозволяють вимкнути компоненти, які ви не використовуєте. Це додатково зменшує розмір:

<PropertyGroup>
  <PublishAot>true</PublishAot>
  
  <!-- Вимкнути невикористані функції для зменшення розміру -->
  <MauiEnableVisualAssemblyScanning>false</MauiEnableVisualAssemblyScanning>
  <_EnableJsonSchemaReader>false</_EnableJsonSchemaReader>
</PropertyGroup>

Комплексна конфігурація проєкту

Добре, давайте зберемо все разом. Ось повний приклад .csproj, що поєднує Native AOT для iOS з оптимізацією для Android:

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

  <!-- Native AOT для iOS та Mac Catalyst -->
  <PropertyGroup Condition="$(TargetFramework.Contains('-ios')) Or $(TargetFramework.Contains('-maccatalyst'))">
    <PublishAot>true</PublishAot>
  </PropertyGroup>

  <!-- Оптимізація AOT для Android -->
  <PropertyGroup Condition="$(TargetFramework.Contains('-android'))">
    <AndroidStripILAfterAOT>true</AndroidStripILAfterAOT>
    <AndroidEnableProfiledAot>true</AndroidEnableProfiledAot>
    <AndroidLinkTool>r8</AndroidLinkTool>
  </PropertyGroup>

  <!-- Загальні налаштування для Release -->
  <PropertyGroup Condition="'$(Configuration)' == 'Release'">
    <DebugSymbols>false</DebugSymbols>
    <DebugType>none</DebugType>
  </PropertyGroup>
</Project>

Скопіюйте, адаптуйте під свій проєкт — і цього має бути достатньо для старту.

Адаптація коду: патерни для AOT-сумісного MAUI додатка

Перехід на Native AOT — це не просто один рядок у .csproj (хоча було б непогано, правда?). Код теж потребує адаптації. Ось ключові патерни, які варто впровадити.

Dependency Injection замість Service Locator

// ❌ Антипатерн — Service Locator з рефлексією
public class OldViewModel
{
    private readonly IMyService _service;
    
    public OldViewModel()
    {
        _service = DependencyService.Get<IMyService>(); // Може не працювати з AOT
    }
}

// ✅ AOT-сумісний патерн — Constructor Injection
public partial class NewViewModel : ObservableObject
{
    private readonly IMyService _service;
    private readonly INavigationService _navigation;
    
    public NewViewModel(IMyService service, INavigationService navigation)
    {
        _service = service;
        _navigation = navigation;
    }
    
    [RelayCommand]
    private async Task LoadDataAsync()
    {
        var data = await _service.GetDataAsync();
        // ...
    }
}

Якщо у вашому проєкті ще десь залишився DependencyService.Get — це перший кандидат на рефакторинг.

Реєстрація сервісів у MauiProgram.cs

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

        // Явна реєстрація — AOT-сумісно
        builder.Services.AddTransient<MainViewModel>();
        builder.Services.AddTransient<MainPage>();
        builder.Services.AddSingleton<IMyService, MyService>();
        
        // HTTP клієнт з Source-Generated JSON
        builder.Services.AddHttpClient<IApiService, ApiService>(client =>
        {
            client.BaseAddress = new Uri("https://api.example.com");
        });

        return builder.Build();
    }
}

Навігація через Shell: правильний підхід

// Реєстрація маршрутів — AOT-сумісно (конкретні типи)
Routing.RegisterRoute(nameof(DetailsPage), typeof(DetailsPage));
Routing.RegisterRoute(nameof(SettingsPage), typeof(SettingsPage));

// Передача параметрів — IQueryAttributable замість QueryPropertyAttribute
public partial class DetailsViewModel : ObservableObject, IQueryAttributable
{
    [ObservableProperty]
    private string _itemId = string.Empty;

    public void ApplyQueryAttributes(IDictionary<string, object> query)
    {
        if (query.TryGetValue("id", out var id))
        {
            ItemId = id?.ToString() ?? string.Empty;
        }
    }
}

Чому саме IQueryAttributable? Справа в тому, що атрибут [QueryProperty] під капотом використовує рефлексію — а це не trim-safe. Інтерфейс IQueryAttributable працює через звичайний виклик методу, тому з AOT все ок.

CI/CD пайплайн з Native AOT

Інтегрувати Native AOT у CI/CD не набагато складніше за звичайну збірку. Ось приклад для GitHub Actions:

# .github/workflows/build-ios.yml
name: Build iOS with Native AOT

on:
  push:
    branches: [ main ]

jobs:
  build-ios:
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup .NET 10
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'
      
      - name: Install MAUI workload
        run: dotnet workload install maui
      
      - name: Publish iOS with Native AOT
        run: |
          dotnet publish -f net10.0-ios -c Release \
            -r ios-arm64 \
            -p:ArchiveOnBuild=true \
            -p:CodesignKey="Apple Distribution" \
            -p:CodesignProvision="MyApp_AppStore"
      
      - name: Upload to TestFlight
        uses: apple-actions/upload-testflight-build@v3
        with:
          app-path: '**/*.ipa'
          issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
          api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }}
          api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }}

Єдине, що варто врахувати — час збірки збільшиться на 2–5 хвилин через повний статичний аналіз. Але це ж CI, тут можна й почекати.

Коли варто і коли не варто використовувати Native AOT

Native AOT — це не чарівна паличка. Є ситуації, де він ідеально підходить, і ситуації, де краще обійтися без нього.

Використовуйте Native AOT, коли:

  • Розмір додатка критичний — наприклад, ви орієнтуєтесь на ринки з повільним інтернетом або пристрої з обмеженою пам'яттю
  • Швидкість запуску важлива — платіжні додатки, месенджери, все що має відкриватися миттєво
  • Ви розробляєте для iOS/Mac Catalyst — тут Native AOT повністю стабільний і перевірений
  • Ваш код вже trim-safe — використовуєте MVVM Toolkit, Source Generators, DI — тоді перехід буде відносно безболісним

Не використовуйте Native AOT, коли:

  • Проєкт залежить від бібліотек із рефлексією — Newtonsoft.Json, старіші версії Prism, та інші бібліотеки без AOT-підтримки
  • Ви активно використовуєте dynamic або Assembly.LoadFile — AOT цього просто не підтримує
  • Цільова платформа — тільки Android — поки що AndroidStripILAfterAOT + Profiled AOT дадуть кращий баланс стабільності та продуктивності
  • Вас турбує час збірки під час розробки — насправді, це не проблема, бо Native AOT працює тільки під час dotnet publish, а не dotnet build

Часті запитання (FAQ)

Чи можна використовувати Native AOT разом з .NET MAUI Blazor Hybrid?

Так, але є нюанси. Blazor Hybrid рендерить UI через WebView, і основні компоненти Blazor нормально працюють з Native AOT. Але рефлексивні бібліотеки на стороні C# потребуватимуть тих самих адаптацій, що описані вище. Тестуйте свій конкретний набір компонентів — універсальної відповіді тут немає.

Чи підтримує Entity Framework Core роботу з Native AOT?

Частково. EF Core у .NET 10 підтримує Compiled Queries, і вони AOT-сумісні. А ось динамічні LINQ-запити та lazy loading — ні. Для мобільних додатків, чесно кажучи, часто краще використовувати SQLite-net із явними SQL-запитами або Dapper — вони простіші й мають кращу AOT-сумісність.

Наскільки Native AOT впливає на час збірки в CI/CD?

Зазвичай збірка стає довшою на 20–40%. Для типового MAUI-додатка на GitHub Actions macOS runner це плюс 2–5 хвилин. Неприємно? Можливо. Але менший бінарник і швидший запуск для користувачів — це варта компенсація.

Що робити, якщо стороння бібліотека видає попередження тримера?

Є кілька варіантів (від кращого до гіршого): зверніться до автора бібліотеки з проханням додати AOT-підтримку, пошукайте альтернативну бібліотеку, або використовуйте DynamicDependencyAttribute та XML-файли тримера для явного збереження потрібних типів. Останній варіант — обхідний шлях, і він може зламатися при оновленні бібліотеки, тому використовуйте з обережністю.

Чи буде Native AOT на Android готовий до продакшну у .NET 11?

За інформацією від Microsoft — так, це в планах. .NET 10 як LTS-реліз отримуватиме оновлення до листопада 2028. Очікується, що Native AOT для Android стане стабільним у .NET 11, або навіть раніше — через сервісні оновлення .NET 10. Слідкуйте за issue #106748 у репозиторії dotnet/runtime.

Про Автора Editorial Team

Our team of expert writers and editors.