Что такое .NET Native AOT — чем он отличается от JIT и trimming

· Обновлено: · · C#, .NET, Native AOT, Публикация, Проектирование

История изменений (1 обновлений, последнее 30 Aug 2026)

Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.

Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
Первая публикация
Цитирование статьи(DOI: 10.5281/zenodo.21619675)

Статья заархивирована на Zenodo. Ниже приведены DOI, который всегда ведёт к последней версии, и DOI, закреплённый за версией, которую вы читаете.

Го Комура (2026). Что такое .NET Native AOT — чем он отличается от JIT и trimming. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21619675 https://comcomponent.com/ru/blog/2026/03/13/001-dotnet-native-aot-what-is/

DOI (последняя версия)
10.5281/zenodo.21619675
DOI (эта версия)
10.5281/zenodo.21619676

В статье Как вызвать C# Native AOT DLL из C/C++ уже разобрано, как с помощью Native AOT вызывать C# из C/C++. Но по-честному сначала стоило объяснить, что такое Native AOT. Порядок статей получился слегка перевёрнутым.

Разговор о Native AOT почти всегда начинается с путаницы в терминах.

  • Это про отказ от JIT?
  • Чем это отличается от self-contained и single-file?
  • Это то же семейство, что и ReadyToRun?
  • Что происходит, когда сыплется куча предупреждений trimming?
  • Можно ли с тем же спокойствием использовать это в WPF / WinForms / ASP.NET Core?

Если свалить всё в одну кучу, Native AOT кажется то «магией, которая просто ускоряет», то «страшилкой из одних ограничений». Оба взгляда слишком грубые.

Как путаница в терминах порождает неверную картинуЕсли смешать JIT, self-contained, ReadyToRun и trimming, Native AOT кажется либо магией ускорения, либо страшилкой из ограничений; оба взгляда слишком грубые.Термины свалены в одну кучуКажется магией ускоренияКажется страшилкой из ограниченийСначала разделить словаСтановится выбором с понятным местом

Рис. 1: Источник путаницы — смешение терминов. Начинать нужно с того, чтобы развести слова.

В этой статье, опираясь в основном на текущую практику .NET 8 и новее, сначала разберём четыре вещи.

  • Что такое Native AOT на самом деле
  • Что он даёт и где становится жёстким
  • Чем он отличается от ReadyToRun и trimming
  • С каких приложений спокойнее начинать

Содержание

  1. Сначала вывод (коротко)
  2. Сначала — сводные таблицы
    • 2.1. Термины вокруг Native AOT
    • 2.2. Различия JIT, ReadyToRun и Native AOT
  3. Общая картина Native AOT (схема)
  4. Что даёт Native AOT
    • 4.1. Запуск обычно становится легче
    • 4.2. Не нужно рассчитывать на заранее установленный runtime
    • 4.3. Подходит для ограниченных сред выполнения
  5. Где Native AOT становится жёстким
    • 5.1. Рефлексия и динамическая генерация кода
    • 5.2. Нужно сразу думать в терминах trimming
    • 5.3. Публикация отдельно под каждую платформу
    • 5.4. Windows-десктоп и COM требуют особой осторожности
  6. Минимальные шаги
    • 6.1. csproj
    • 6.2. publish
    • 6.3. Как писать JSON
  7. Где это уместно
  8. Где это не стоит
  9. Типичные ловушки
  10. Итог
  11. Источники

Карта знаний этой статьи

.NET Native AOT — это способ публикации, при котором приложение заранее компилируется в нативный код на этапе publish и не использует JIT во время выполнения. Так проще сократить время запуска и потребление памяти и проще распространять приложение в среды без установленного runtime .NET. ReadyToRun оставляет IL и лишь частично переносит работу JIT на более ранний этап; Native AOT на JIT во время выполнения вообще не рассчитывает, поэтому trimming почти обязателен, а свободная рефлексия, динамическая генерация кода и built-in COM сочетаются с ним плохо. В Windows у Native AOT нет built-in COM: WPF плохо совместим с trimming, WinForms сильно зависит от built-in COM marshalling, и в обоих случаях поддержка trimming отключена в .NET SDK, поэтому как первый кандидат на Native AOT их стоит рассматривать осторожно. Если COM нужен, сильный вариант — перепроектировать решение вокруг ComWrappers.

Карта знаний .NET Native AOTСхема, которая показывает, чем Native AOT отличается от JIT и ReadyToRun, как он сталкивается с trimming, рефлексией и built-in COM, и почему WPF и WinForms требуют осторожностинесовместимо стребуеттребуетнесовместимо снесовместимо снесовместимо срекомендуется длянесовместимо стребуетне рекомендуетсяне рекомендуетсяиспользуетснижаеттребуетиспользуеттребуетNative AOTJIT-компиляция (Just-In-Time)ReadyToRuntrimming (обрезка)рефлексия (.NET)динамическая генерация кода (Reflection.Emit и др.)built-in COM interopComWrappers / source-generated COMWPFWindows Formssource generatorsource generation в System.Text.Jsonself-contained публикацияsingle-file публикация.NET (начиная с Core)

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 16, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle

1. Сначала вывод (коротко)

  • Native AOT — способ публикации .NET-приложения: на этапе publish код заранее компилируется в нативный и уже так распространяется.
  • JIT во время выполнения не используется, поэтому время запуска и потребление памяти обычно улучшаются, а приложение проще раздавать в среды без установленного runtime .NET.
  • Но свободная рефлексия, динамическая генерация кода, built-in COM и библиотеки без поддержки trimming сочетаются с ним плохо.
  • То есть это не волшебная кнопка ускорения, а модель публикации, которая ради запуска, распространения и среды выполнения чуть отступает от динамического мира в сторону статического.

Native AOT — это механизм, чтобы раздавать .NET в виде, близком к нативному приложению, а не галочка «ускорить компиляцию».

Что получаем и от чего отказываемсяNative AOT за счёт предварительной компиляции на этапе publish даёт более лёгкий запуск, меньшее потребление памяти и публикацию без отдельного runtime, но взамен отказывается от свободной рефлексии, динамической генерации кода и built-in COM.Native AOTЧто получаемОт чего отказываемсяЛёгкий запуск, память и распространениеСвободная рефлексияДинамическая генерация кода и built-in COM

Рис. 2: Это не магия ускорения, а обмен: чуть отойти от динамического мира в сторону статического.

2. Сначала — сводные таблицы

2.1. Термины вокруг Native AOT

Если сразу развести эти слова, дальше будет проще.

Термин Что делает Связь с Native AOT
JIT Во время выполнения строит нативный код из IL Native AOT делает эту работу заранее
self-contained Раздаёт вместе с приложением весь нужный набор .NET Native AOT относят к этому же семейству
single-file Собирает дистрибутив в один файл К сути Native AOT это не относится, но внешне результат часто похож
trimming Удаляет неиспользуемый код В Native AOT это почти обязательное условие
ReadyToRun Оставляет IL и частично переносит работу JIT на более ранний этап Похоже по названию, но по сути другая вещь
source generator Переносит динамическую работу со времени выполнения на генерацию кода при сборке Хорошо сочетается с Native AOT

Путаница чаще всего из-за того, что Native AOT — не одна функция, а модель публикации, которая работает вместе с self-contained, trimming, source generation и publish с фиксированным RID.

Механизмы, с которыми Native AOT работает вместеNative AOT — не одна функция, а модель публикации, которая работает вместе с self-contained, trimming, source generation и publish с фиксированным RID.Native AOT (модель публикации)Семейство self-containedtrimming почти обязателенХорошо сочетается с source generatorpublish с фиксированным RID

Рис. 3: Native AOT — не отдельная функция, а модель публикации в связке с соседними механизмами.

2.2. Различия JIT, ReadyToRun и Native AOT

И здесь быстрее один раз посмотреть таблицу.

Критерий Обычное выполнение с JIT ReadyToRun Native AOT
JIT во время выполнения Используется Иногда всё ещё используется Не используется
Содержимое дистрибутива В основном IL IL + заранее сгенерированный код В основном нативный исполняемый файл
Запуск Базовый уровень Легко улучшить Улучшается очень заметно
Совместимость Самая широкая Широкая Ограничения жёсткие
Динамические возможности Легко использовать В целом легко использовать Много ограничений
Для какой цели подходит Обычная разработка на .NET в целом Сначала улучшить запуск Целенаправленно взять запуск, распространение и ограниченные среды

Если ReadyToRun — это «чуть разгрузить JIT», то Native AOT — «вообще не рассчитывать на JIT во время выполнения». Хотя в обоих названиях есть AOT, по сути это совсем разные вещи.

Разные направления ReadyToRun и Native AOTReadyToRun оставляет IL и чуть переносит работу JIT на более ранний этап, Native AOT на JIT во время выполнения не рассчитывает; одно и то же слово AOT скрывает совсем разный подход.Чуть разгрузитьНе рассчитывать на негоЧто делать с JIT?ReadyToRun (IL остаётся)Native AOT (нативный код в центре)Совместимость остаётся широкойЗапуск быстрый, ограничения жёсткие

Рис. 4: Даже с одним словом AOT «разгрузить JIT» и «не рассчитывать на JIT» — разные вещи.

Если поверх этого свести «какую форму публикации выбрать» на одну схему, получится так.

Как выбрать форму публикацииСначала — можно ли поставить runtime .NET на стороне получателя, затем — насколько можно сократить динамические механизмы.МожноНе настолькоНужноНе хочется / нельзяЕстьНетДаНетНужно выбрать форму публикацииМожно ли поставить runtime .NET у получателя?Нужно ли поджать время запуска?framework-dependentобычная публикацияReadyToRunулучшить запуск, сохранив совместимостьЕсть зависимость от reflection, динамической генерации кода, built-in COM?self-containedпри необходимости собрать в single-fileЦель — console / worker / небольшой API?Native AOTСначала self-containedсократить динамические зависимости и вернуться к вопросу

Рис. 5: Как выбирать форму публикации. Сначала — можно ли поставить runtime, затем — можно ли сократить динамические механизмы.

Первая развилка — можно ли поставить runtime на стороне получателя, следующая — насколько можно сократить динамические механизмы. self-contained и single-file — про «как раздавать», ReadyToRun и Native AOT — про «когда строить нативный код», поэтому на практике их комбинируют.

3. Общая картина Native AOT (схема)

Если грубо нарисовать Native AOT, получится так.

Общая схема Native AOTОбычное выполнение делает JIT во время работы, а Native AOT переносит анализ, удаление лишнего кода и генерацию нативного кода на этап publish.Обычное выполнениеdotnet publish + PublishAotИсходный код C# / .NETIL-сборкиJIT во время выполненияВыполнение приложенияAOT- / trim-анализУдаление неиспользуемого кодаГенерация нативного кодаИсполняемый файл для конкретного RID

Рис. 6: Обычное выполнение делает JIT во время работы, Native AOT переносит анализ, обрезку и генерацию нативного кода на этап publish.

Обычный .NET сначала строит IL, а во время выполнения JIT-компилирует только нужное. Native AOT переносит большую часть этой поздней стадии на этап publish.

Здесь важно, что уже на этапе publish нужно знать почти весь код, который понадобится во время выполнения. Именно здесь меняются допустимые предпосылки к коду.

  • Находить типы во время выполнения
  • Порождать код во время выполнения
  • Загружать сборки (Assembly) во время выполнения
  • Откладывать разрешение «как-нибудь потом разберёмся»

Такой стиль внезапно перестаёт сочетаться с Native AOT.

На этапе publish нужно знать почти всёNative AOT на этапе publish должен знать почти весь код, который понадобится во время выполнения, поэтому поиск типов, порождение кода, загрузка Assembly и отложенное разрешение сочетаются с ним плохо.На publish зафиксировать нужный кодИскать типы во время выполненияПорождать код во время выполненияЧитать Assembly во время выполненияОбходиться отложенным разрешениемВсё это плохо сочетается с AOT

Рис. 7: Главная смена предпосылки. Чем сильнее код «решает во время выполнения», тем сильнее он сталкивается с Native AOT.

4. Что даёт Native AOT

4.1. Запуск обычно становится легче

Самый заметный эффект Native AOT — всё же запуск.

  • CLI-инструменты
  • Короткоживущие процессы
  • Запуск в духе serverless
  • Запуск и замена контейнеров
  • Инструменты мониторинга и небольшие постоянно живущие процессы

В таких сценариях стоимость JIT хорошо видна, и Native AOT переносит её заранее, поэтому первый момент запуска становится легче.

Потребление памяти тоже обычно улучшается — это выгодно, когда на том же железе нужно уместить больше экземпляров. Особенно разница постепенно накапливается в облаке, где одновременно поднимается много копий одного процесса.

4.2. Не нужно рассчитывать на заранее установленный runtime

Приложение, опубликованное через Native AOT, проще запускать в средах без установленного runtime .NET.

Это скромно звучит, но на практике это большой плюс.

  • Не хочется говорить получателю: «сначала поставьте .NET 9 Runtime»
  • Хочется сделать образ контейнера тоньше
  • Хочется положить один небольшой инструмент и просто запустить
  • Не хочется разрешать JIT в среде выполнения — или это запрещено

В таких случаях уже само отсутствие предпосылки «runtime нужно готовить отдельно» сильно упрощает разговор.

«Runtime не нужен» здесь значит: получателю не требуется отдельно ставить .NET. Это не значит, что эквивалент runtime полностью исчезает из самого приложения.

Что на самом деле значит «runtime не нужен»«Runtime не нужен» для Native AOT значит, что получателю не нужно отдельно ставить .NET, а не то, что эквивалент runtime полностью исчезает из приложения.Слова «runtime не нужен»Получателю не нужно ставить .NETЭквивалент runtime из приложения не исчезаетМеньше предпосылок к распространению и запуску

Рис. 8: «Runtime не нужен» — про сторону получателя. Эквивалент runtime входит в само приложение.

4.3. Подходит для ограниченных сред выполнения

Native AOT не использует JIT во время выполнения, поэтому его проще запускать и там, где JIT не разрешён.

Этот эффект сильнее проявляется в облаке, контейнерах и мобильных сценариях, чем на десктопе. Но и в контексте разработки под Windows это ощутимый плюс: меньше лишних предпосылок на стороне получателя.

5. Где Native AOT становится жёстким

5.1. Рефлексия и динамическая генерация кода

Главное ограничение Native AOT — здесь.

  • Динамическая загрузка вроде Assembly.LoadFile
  • Генерация кода во время выполнения вроде System.Reflection.Emit
  • Рефлексия, которая без ограничений обходит типы во время выполнения
  • Код, который свободно собирает generic-типы во время выполнения

Такой код трудно однозначно зафиксировать на этапе publish, поэтому именно он становится рассадником предупреждений AOT.

Разумеется, не настолько просто, что одна строка рефлексии сразу всё ломает. Но тенденция устойчива: чем сильнее дизайн склоняется к «посмотрим во время выполнения и решим», тем тяжелее.

Среди имён предупреждений часто встречаются относящиеся к семейству RequiresDynamicCode. Это значит «этот вызов может сломаться под AOT», поэтому такие предупреждения безопаснее не подавлять без разбора.

При работе с Native AOT удобно держать в голове простое правило: меньше «сообразительности во время выполнения», больше «явности на этапе сборки».

От сообразительности во время выполнения к явности на сборкеДинамическая загрузка, генерация кода во время выполнения и неограниченная рефлексия становятся рассадником предупреждений AOT, поэтому дизайн сдвигают от решений во время выполнения к явному описанию на этапе сборки.Дизайн, который полагается на решения во время выполненияПредупреждения семейства RequiresDynamicCodeУвеличить явность на этапе сборкиНа publish нужный код зафиксированНе подавлять без разбора

Рис. 9: К главному ограничению один ход: «решать во время выполнения» заменить на «явно описать на сборке».

5.2. Нужно сразу думать в терминах trimming

Native AOT тесно связан с trimming. Здесь легко упустить, что значение имеет не только ваш код, но и то, как написаны зависимости.

Особого внимания заслуживают такие вещи.

  • Сериализаторы на основе рефлексии
  • DI и плагинные схемы, которые собирают типы сканированием во время выполнения
  • Механизмы, которые ищут тип по строковому имени и создают экземпляр
  • Библиотеки, которые опираются на динамические прокси или генерацию IL

Если здесь есть предупреждения, а вы решаете «publish прошёл — значит всё хорошо», позже это выйдет боком. В Native AOT предупреждения почти всегда стоит читать всерьёз.

Успех trimming решает не только ваш кодНа trimming влияет не только ваш код, но и зависимости: сериализаторы на рефлексии, DI со сканированием во время выполнения, поиск типа по имени и библиотеки с динамическими прокси требуют внимания.Успех или провал trimmingКак написан ваш кодКак написаны зависимостиРефлексия и сканирование во время выполненияЧитать предупреждения всерьёз и проверять

Рис. 10: Легко упустить сторону зависимостей. «Publish прошёл — значит всё хорошо» позже становится дорогим.

Одной фразы «читайте всерьёз» недостаточно, чтобы двигаться дальше, поэтому ниже — приоритет действий. Официальная документация тоже советует пробовать в этом порядке.

Порядок Действие Когда применять
1 Отказаться от reflection Activator.CreateInstance(Type) можно заменить generic-аргументом или сместить работу на source generator
2 Поставить DynamicallyAccessedMembers reflection нужна, но целевой тип известен на этапе компиляции
3 Поставить RequiresUnreferencedCode тип выбирается строкой во время выполнения и статически разобрать это нельзя
4 Подавить через UnconditionalSuppressMessage всё предыдущее невозможно, и вы уже убедились, что это безопасно — последнее средство

Самый частый первый шаг — вариант 2: «тип известен, а предупреждение всё равно есть». Например, следующий код даёт IL2070.

// IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods'
void PrintMethodNames(Type type)
{
    foreach (var method in type.GetMethods())
    {
        Console.WriteLine(method.Name);
    }
}

Если вызываете GetMethods(), объявите атрибутом, что публичные методы нужно сохранить, — и предупреждение исчезнет.

using System.Diagnostics.CodeAnalysis;

void PrintMethodNames(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    foreach (var method in type.GetMethods())
    {
        Console.WriteLine(method.Name);
    }
}

// Если вызывающий код передаёт typeof, требование выполняется само
PrintMethodNames(typeof(DateTime));

Вызываемый API и нужная аннотация в целом соответствуют друг другу. Для GetMethod / GetMethods это PublicMethods, для GetProperty / GetPropertiesPublicProperties, для Activator.CreateInstancePublicParameterlessConstructor или PublicConstructors. DynamicallyAccessedMemberTypes.All удобно, но оставляет все члены целевого типа, раздувает размер, и оставленные члены часто приносят следующие предупреждения, поэтому принцип такой: указывать минимум необходимого.

Если атрибут добавили, а предупреждение не исчезло, поднимитесь от места с reflection к вызывающему коду и проверьте, что атрибут стоит на всём пути. Если выпало одно звено, требование обрывается там.

Если атрибут добавили, а предупреждение осталосьDynamicallyAccessedMembers указывают в минимуме необходимого; если предупреждение не исчезает, поднимаются от места с reflection к вызывающему коду и проверяют весь путь. Одно выпавшее звено обрывает требование.Атрибут есть, предупреждение осталосьПодняться к вызывающему кодуПроверить, что атрибут стоит на всём путиОдно выпавшее звено обрывает требованиеУказывать минимум необходимого

Рис. 11: Атрибут ставят не в одну точку, а на путь. Если выпало одно звено, требование обрывается там.

5.3. Публикация отдельно под каждую платформу

Native AOT публикуют с фиксированным RID (Runtime Identifier). То есть собранное для win-x64 нельзя просто взять и запустить на linux-x64.

  • Windows x64
  • Windows Arm64
  • Linux x64
  • Linux Arm64
  • macOS Arm64

Предполагается, что для каждой целевой платформы готовят свой артефакт.

Здесь ощущения гораздо ближе к «настоящему нативному приложению», чем при обычной framework-dependent публикации .NET.

5.4. Windows-десктоп и COM требуют особой осторожности

В контексте работы KomuraSoft этот момент особенно важен.

В Windows у Native AOT нет built-in COM. Плюс WPF плохо сочетается с trimming, а WinForms сильно зависит от built-in COM marshalling, поэтому по крайней мере сейчас оба варианта как «первый кандидат на Native AOT» стоит рассматривать очень осторожно.

Зафиксируем, что здесь значит «сейчас». Описание в этой статье опирается на официальную документацию поколений .NET 8 / 9 / 10 (по состоянию на июль 2026). По WPF и WinForms на странице Microsoft «Known trimming incompatibilities» прямо сказано: WPF сильно зависит от reflection и проверки кода во время выполнения и после trimming почти не работает, WinForms сильно зависит от built-in COM marshalling, поэтому поддержка trimming для обоих отключена на стороне .NET SDK. То есть речь уже не о «стоит быть осторожным»: сейчас это останавливает сам SDK. В будущем формулировка может измениться — на момент чтения сверьтесь с той же страницей.

Почему WPF и WinForms остановленыУ Native AOT в Windows нет built-in COM; WPF из-за зависимости от reflection и проверки кода во время выполнения после trimming почти не работает, WinForms сильно зависит от built-in COM marshalling, поэтому поддержка trimming для обоих отключена в .NET SDK.Native AOT в WindowsНет built-in COMWPFСильная зависимость от reflectionWinFormsСильная зависимость от COM marshallingSDK отключает trimming

Рис. 12: Дело не в осторожности: сейчас это останавливает SDK. Само десктопное приложение не берите первым кандидатом.

Итого,

  • сразу переводить на Native AOT само приложение WPF / WinForms
  • переносить COM interop как есть, с привычными для обычного .NET приёмами

в таких шагах предупреждения на publish и ограничения во время выполнения нарастают сразу, и стоимость сопровождения легко взлетает.

И наоборот,

  • консоль
  • worker
  • небольшой Web API
  • компоненты для связи с нативным кодом, которые легко свести к границе C-функций

как точка входа естественнее.

Если COM нужен, в части случаев разумнее остаться на JIT либо перепроектировать решение вокруг ComWrappers / source-generated COM.

6. Минимальные шаги

Сначала важное: для publish Native AOT нужен нативный набор инструментов. Если просто поставить PublishAot и сразу вызвать dotnet publish, падение будет не на компиляции, а на финальной нативной компоновке. Это первый барьер — поставьте toolchain заранее.

Среда Что нужно
Windows Visual Studio 2022 или новее. Рабочую нагрузку «Разработка классических приложений на C++» ставят со всеми компонентами по умолчанию
Ubuntu 18.04 и новее sudo apt-get install clang zlib1g-dev
Alpine 3.15 и новее sudo apk add clang build-base zlib-dev
Fedora 39 и новее / RHEL 8 и новее sudo dnf install clang zlib-ng-devel zlib-ng-compat-devel zlib-devel
macOS Xcode Command Line Tools (поддерживается с .NET 8)

По сути это компиляторный toolchain и пакеты разработки библиотек, от которых зависит runtime .NET. Если гоняете это в CI, в Native AOT-примерах dotnet/samples есть Dockerfile и для Linux, и для Windows — оттуда быстрее всего взять шаги установки.

Ещё: двоичный файл, собранный на Linux, работает только на том же Linux или более новом. Собранное на Ubuntu 20.04 пойдёт на 20.04 и новее, на 18.04 — нет. Выбор среды сборки сразу задаёт диапазон, куда можно распространять.

Барьер до publish и диапазон распространенияДля publish Native AOT нужен нативный toolchain; без него падение на финальной нативной компоновке. Двоичный файл, собранный на Linux, работает только на том же Linux или более новом, поэтому выбор среды сборки задаёт диапазон распространения.НетЕстьЕсть ли toolchain?Падение на нативной компоновкеПоявляется двоичный файл на каждый RIDНа Linux работает только на той же или более новой средеВыбор среды сборки задаёт диапазон распространения

Рис. 13: Первый барьер — toolchain. На Linux «возраст» среды сборки задаёт, куда можно распространять результат.

6.1. csproj

Сначала добавьте PublishAot в файл проекта.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <PublishAot>true</PublishAot>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

Для примера достаточно net8.0. Сам подход почти не меняется и в .NET 9 / 10.

Важно не вешать это временно только на командную строку dotnet publish, а держать флаг в проекте постоянно, чтобы анализ на build / publish видеть в повседневной работе.

При этом <PublishAot>true</PublishAot> не превращает в Native AOT сразу и обычные локальные запуски. Повседневный dotnet run и обычное выполнение по-прежнему идут через JIT, настоящая компиляция Native AOT — на этапе publish.

Повседневная работа после PublishAotДаже если PublishAot стоит в проекте, повседневный dotnet run остаётся на JIT, а компиляция Native AOT идёт на publish. Поэтому флаг держат в проекте постоянно и смотрят анализ на build и publish.Поставить PublishAot в csprojПовседневный dotnet run остаётся на JITНа publish идёт AOT-компиляцияСмотреть анализ и предупреждения каждый день

Рис. 14: Повседневный запуск — JIT, настоящая работа — publish. Поэтому настройку не вешают временно, а держат постоянно и смотрят анализ.

6.2. publish

Например, для Windows x64 это выглядит так.

dotnet publish -c Release -r win-x64

Для Linux x64 — так.

dotnet publish -c Release -r linux-x64

Результат привязан к RID. Взгляд смещается от «одной .NET DLL, которая работает везде» к «исполняемому файлу, собранному под конкретные ОС и архитектуру».

Если заходите со стороны Web API, проще начать с шаблона, изначально рассчитанного на Native AOT.

dotnet new webapiaot -o MyFirstAotWebApi

Для worker — этот.

dotnet new worker -o WorkerWithAot --aot

6.3. Как писать JSON

Незаметная, но частая точка столкновения с Native AOT — JSON. System.Text.Json в привычном стиле легко скатывается к reflection, поэтому спокойнее опираться на source generation.

using System.Text.Json;
using System.Text.Json.Serialization;

[JsonSerializable(typeof(AppConfig))]
internal partial class AppJsonContext : JsonSerializerContext
{
}

public sealed class AppConfig
{
    public string? Name { get; init; }
    public int RetryCount { get; init; }
}

var config = new AppConfig
{
    Name = "sample",
    RetryCount = 3
};

string json = JsonSerializer.Serialize(config, AppJsonContext.Default.AppConfig);

На практике проще запомнить не как «сделать код совместимым с Native AOT», а как ориентир: не заставлять среду выполнения искать типы. Так реже промахиваются.

7. Где это уместно

Native AOT особенно хорошо ложится в такие сценарии.

  • CLI / инструменты, где главное — запуск
  • Небольшие API, которые разворачивают во множестве контейнеров
  • Worker / фоновые службы
  • Serverless-сценарии и короткоживущие процессы
  • Небольшие .NET-компоненты внутри нативного приложения
  • Ситуации, где не хочется требовать заранее установленный runtime .NET

Общее у них то, что границы относительно чёткие и динамические механизмы проще сократить.

8. Где это не стоит

И наоборот, есть ясные случаи, где Native AOT с самого начала не стоит делать основной ставкой.

  • Само существующее крупное приложение на WPF / WinForms
  • Схемы, построенные на built-in COM interop
  • Приложения, где главная роль у загрузки плагинов во время выполнения
  • Конфигурации с сильной зависимостью от фреймворка, который ищет типы через рефлексию
  • Библиотеки, для которых System.Reflection.Emit или динамические прокси — обычное дело
  • Проектирование через слой C++/CLI

В таких случаях разумнее обычный .NET с JIT, ReadyToRun или пересмотр границ архитектуры.

9. Типичные ловушки

Напоследок — куда легко наступить при первом знакомстве с Native AOT.

  • Считать предупреждения publish мелочью
    • Как уже сказано, предупреждения Native AOT нужно читать всерьёз.
  • Build проходит, а publish ломается
    • На этапе publish анализ по-настоящему охватывает и зависимости, поэтому часть вещей видна только здесь.
  • Относиться к ReadyToRun и Native AOT одинаково
    • Слова похожи, сила ограничений — нет.
  • Начинать сразу с самого десктопного приложения
    • Спокойнее начать с console / worker / небольшого API.
  • Писать JSON или привязку конфигурации в привычном стиле
    • Код, рассчитанный на reflection, аукнется позже.
  • Раздавать артефакт так, будто он не зависит от платформы
    • Дистрибутив Native AOT привязан к RID.
  • Считать, что «Native AOT = всё становится быстрее»
    • Главное здесь — запуск, распространение и среда выполнения. Если это упустить, ожидания разъедутся с реальностью.

В Native AOT окончательно решает не dotnet build, а dotnet publish. Если начать гонять publish пораньше, на поздних этапах будет меньше сюрпризов.

Решает publishBuild может проходить, а publish — ломаться, потому что на publish анализ по-настоящему охватывает и зависимости; окончательно Native AOT решает не dotnet build, а dotnet publish.dotnet build проходитЕщё не решено, получится лиdotnet publishАнализ с зависимостями идёт всерьёзВот здесь впервые видно, получится лиПоэтому publish гоняют пораньше

Рис. 15: Общий знаменатель ловушек. Не успокаивайтесь на «build прошёл» — гоняйте publish пораньше.

10. Итог

Одной фразой Native AOT — это механизм, который сдвигает .NET-приложение от динамической модели выполнения к модели распространения, которую проще зафиксировать статически.

Смотреть стоит на эти пять пунктов.

  1. Native AOT заранее компилирует код в нативный на этапе publish
  2. Хорошо работает для запуска, памяти и распространения
  3. Взамен жёсток к reflection, динамической генерации кода, built-in COM и коду без поддержки trimming
  4. Первой целью спокойнее брать console / worker / небольшой API, а не само десктопное приложение
  5. Важно снимать предупреждения и проверять всё через publish как можно раньше

Native AOT — не стандартный переключатель на любое .NET-приложение. Но там, где важен запуск, хочется облегчить распространение и сократить предпосылки к среде выполнения, это очень сильный инструмент.

И наоборот, в плотном мире WPF / WinForms / COM во многих случаях пока разумнее обычный .NET. Если научиться это различать, Native AOT перестаёт быть «сложной новой функцией» и становится вариантом с понятным местом применения.

11. Источники

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Что такое Native AOT?
Это способ публикации .NET-приложения, при котором код заранее компилируется в нативный код на этапе publish и распространяется уже в таком виде. JIT во время выполнения не используется, поэтому время запуска и потребление памяти обычно улучшаются, а приложение проще раздавать в среды без установленного runtime .NET. Взамен хуже сочетается со свободной рефлексией, динамической генерацией кода, built-in COM и библиотеками без поддержки trimming. Это не волшебная кнопка ускорения, а модель публикации, которая ради запуска, распространения и среды выполнения чуть отступает от динамического мира в сторону статического.
Чем Native AOT отличается от ReadyToRun?
ReadyToRun оставляет IL и лишь частично переносит работу JIT на более ранний этап, поэтому JIT во время выполнения всё ещё может понадобиться. Совместимость широкая, динамические возможности в основном остаются удобными. Native AOT на JIT во время выполнения не рассчитывает: дистрибутив строится вокруг нативного исполняемого файла, запуск улучшается заметно сильнее, но и ограничения жёстче. Хотя в обоих названиях есть AOT, направление разное: ReadyToRun — «чуть разгрузить JIT», Native AOT — «не рассчитывать на JIT», и по сути это совсем разные вещи.
Можно ли использовать Native AOT в приложениях на WPF или WinForms?
Сейчас к этому стоит относиться очень осторожно. У Native AOT в Windows нет built-in COM, WPF плохо сочетается с trimming, а WinForms сильно зависит от built-in COM marshalling, поэтому оба варианта плохо подходят как первый кандидат на Native AOT. Если COM нужен, в части случаев разумнее остаться на JIT либо перепроектировать решение вокруг ComWrappers / source-generated COM. Как точка входа естественнее консоль, worker и небольшой Web API.
Для каких приложений подходит Native AOT?
Он уместен для CLI-инструментов, где главное — запуск, небольших API, которые разворачивают во множестве контейнеров, worker и фоновых служб, serverless-сценариев и короткоживущих процессов, небольших .NET-компонентов внутри нативного приложения и ситуаций, где не хочется требовать заранее установленный runtime .NET. Общее у них то, что границы относительно чёткие и динамические механизмы проще сократить. И наоборот: он не подходит, если главная роль у загрузки плагинов во время выполнения или если конфигурация сильно завязана на фреймворк, который ищет типы через рефлексию.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог