.NET Native AOTとは - JITやtrimmingとの違い

· 更新日: · · C#, .NET, Native AOT, 発行, 設計

更新履歴(6件・最終更新 2026年08月22日)

この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。

追加した図のMermaidソースの字下げを記事内の規約に統一し、レビュー指摘のあった図の表現を本文の記述に合わせて調整しました。本文の文章は変えていません。
本文の流れ・対比を図でも追えるように、Mermaid図を13点追加しました(地の文500〜750字につき1図の規約に合わせたものです)。既存の2図にはキャプションを追加しています。本文の文章は変えていません。
記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
前提ツールチェーンの表を追加しました(Windowsでは「C++によるデスクトップ開発」ワークロード、各Linuxディストリビューションのインストールコマンド、macOSはXcode Command Line Tools)。Linuxでビルドしたバイナリは同じか新しいLinuxでしか動かない点も追記しています。トリミング警告の対処優先順位の表とIL2070の直す前と後の例、配布形態の選択フロー図を追加しました。あわせてWPFとWinFormsについて「慎重に見たほうがよい」としていた記述を、SDK側でトリミングのサポートが無効化されているという事実に直しました。
本文中の関連記事へのリンクの文言が、リンク先の現在のタイトルと食い違っていたのを、実際のタイトルに揃えました。本文の内容は変えていません。
初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589629)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「.NET Native AOTとは - JITやtrimmingとの違い」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589629 https://comcomponent.com/blog/2026/03/13/001-dotnet-native-aot-what-is/

DOI(最新版)
10.5281/zenodo.21589629
DOI(この版)
10.5281/zenodo.21732657

すでに C# Native AOT DLLをC/C++から呼び出す方法 では、Native AOT を使って C/C++ から C# を呼ぶ話を書きました。 ただ、本当はその前に そもそも Native AOT とは何か を置いたほうが親切でした。少しだけ順番が前後しています。

Native AOT の話は、とにかく最初に用語が混ざりやすいです。

  • JIT をなくす話なのか
  • self-contained や single-file と何が違うのか
  • ReadyToRun と同じ系統なのか
  • trimming warning が大量に出るのは何が起きているのか
  • WPF / WinForms / ASP.NET Core でも同じ温度感で使えるのか

このへんが一緒くたになると、Native AOT が「ただ速くなる魔法」に見えたり、逆に「制約だらけで怖いもの」に見えたりします。どちらも少し雑です。

用語が混ざると起きる誤解JITやself-contained、ReadyToRun、trimmingといった用語が一緒くたになると、Native AOTがただ速くなる魔法に見えたり制約だらけで怖いものに見えたりするが、どちらも雑な見方である。用語が一緒くたになる速くなる魔法に見える制約だらけで怖く見える言葉を分けて整理する使いどころの見える選択肢に

図1: 誤解の原因は用語の混線。まず言葉を分けるところから始める。

この記事では、主に .NET 8 以降の現在の実務感を前提に、次の 4 つを先に整理します。

  • 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. ランタイム事前インストールを前提にしなくてよい
    • 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は、実行時JITを使わずpublish時にネイティブコードへ事前コンパイルする発行方式で、起動時間とメモリ占有を改善しやすく、.NETランタイム未インストールの環境にも配りやすい。ReadyToRunがILを残しつつJITを一部前倒しする方向であるのに対し、Native AOTは実行時JIT自体を前提にしない分、trimmingがほぼ前提になり、自由なリフレクションや動的コード生成、built-in COMとは相性が悪くなる。WindowsのNative AOTにはbuilt-in COMが無く、WPFはtrimmingとの相性の悪さから、WinFormsはbuilt-in COM marshallingへの依存の重さから、いずれも.NET SDK側でtrimmingのサポートが無効化されており、最初のNative AOT候補としては慎重に扱うべきとされる。COMが必要な場合は、ComWrappersを前提に設計し直すことが有力な選択肢になる。

.NET Native AOTの知識マップNative AOTがJITやReadyToRunとどう違い、trimmingやリフレクション・built-in COMとどのように衝突し、WPFやWinFormsがなぜ慎重な扱いになるのかを示す図両立しない前提とする前提とする両立しない両立しない両立しない推奨される対応両立しない前提とする用いるのは非推奨用いるのは非推奨利用する軽減する前提とする利用する前提とするNative AOTJIT(Just-In-Time)コンパイルReadyToRuntrimming(トリミング)リフレクション動的コード生成(Reflection.Emit等)built-in COM interopComWrappers / source-generated COMWPFWindows Formssource generatorSystem.Text.Jsonのsource generationself-contained発行single-file発行.NET(Core以降)

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全16件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

1. まず結論(ひとことで)

  • Native AOT は、.NET アプリを publish 時にネイティブコードへ事前コンパイルして配布する方式です。
  • 実行時 JIT を使わないので、起動時間とメモリ占有が良くなりやすく、.NET ランタイム未インストール環境にも配りやすいです。
  • ただし、自由なリフレクション、動的コード生成、built-in COM、trimming 非対応ライブラリとは相性が悪くなります。
  • つまり、速くなる魔法というより、起動・配布・実行環境の都合のために、動的な世界を少し捨てて静的な世界へ寄せる発行モデルです。

Native AOT は「.NET をネイティブっぽく配るための仕組み」であって、単なるコンパイル高速化のチェックボックスではありません。

何を得て何を捨てるかNative AOTはpublish時の事前コンパイルによって起動時間・メモリ占有・ランタイム不要の配布を得る代わりに、自由なリフレクション・動的コード生成・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 配布物を 1 ファイルにまとめる Native AOT の本質とは別ですが、結果の見た目は近くなりやすい
trimming 使っていないコードを削る Native AOT ではほぼ前提になる
ReadyToRun IL を残したまま、JIT の仕事を少し前倒しする Native AOT とは似て非なる別物
source generator 実行時の動的処理を、ビルド時コード生成へ寄せる Native AOT と相性がよい

混ざりやすいのは、Native AOT が 1 個の機能というより、self-contained、trimming、source generation、RID 固定 publish などと仲良く動く発行モデルであることです。

Native AOTと一緒に動く仕組みNative AOTは1個の機能ではなく、self-contained、trimming、source generation、RID固定publishと組み合わさって動く発行モデルであることを示す。Native AOT〔発行モデル〕self-containedの系統trimmingはほぼ前提source generatorと相性良RID固定でpublish

図3: Native AOTは単独機能ではなく、周辺の仕組みとセットで動く発行モデル。

2.2. JIT / ReadyToRun / Native AOT の違い

ここも最初に 1 枚で見たほうが早いです。

観点 ふつうの JIT 実行 ReadyToRun Native AOT
実行時 JIT 使う まだ使う場面がある 使わない
配布物の中身 IL 中心 IL + 事前生成コード ネイティブ実行ファイル中心
起動 基準 改善しやすい かなり改善しやすい
互換性 いちばん広い 広い 制約が強い
動的機能 使いやすい だいたい使いやすい 制限が多い
向いている目的 ふつうの .NET 開発全般 まずは起動改善したい 起動・配布・制限環境を強く取りにいく

ReadyToRun が「JIT を少し楽にする」方向だとすれば、Native AOT は「実行時 JIT 自体を前提にしない」方向です。 同じ AOT という言葉が付いていても、温度感はかなり違います。

ReadyToRunとNative AOTの方向の違いReadyToRunはILを残したままJITの仕事を少し前倒しして楽にする方向で、Native AOTは実行時JIT自体を前提にしない方向であり、同じAOTという言葉でも温度感がかなり違う。少し楽にする前提にしないJITをどうしたいかReadyToRun〔ILは残る〕Native AOT〔ネイティブ中心〕互換性は広いまま起動は速いが制約は強い

図4: 同じAOTでも、「JITを楽にする」と「JITを前提にしない」では別物。

そのうえで、「では結局どの配布形態を選ぶか」を 1 枚にすると、こうなります。

入れられるそこまでではない詰めたい入れたくない / 入れられない依存する依存しないはいいいえ配布形態を決めたい配布先に .NET ランタイムを入れられる?起動時間を詰めたい?framework-dependentふつうの発行ReadyToRun互換性を保ったまま起動改善reflection・動的コード生成・built-in COM に依存する?self-contained必要なら single-file でまとめる対象は console / worker / 小さな API?Native AOTまず self-contained動的な依存を減らしてから再検討

図5: 配布形態の選び方。ランタイムを置けるか、動的な仕組みを減らせるかの順に分岐する。

最初の分岐は ランタイムを配布先に置けるかどうか、次の分岐は 動的な仕組みをどこまで減らせるか です。 self-contained と single-file は「配り方」の話、ReadyToRun と Native AOT は「いつネイティブコードを作るか」の話なので、実際には組み合わせて考えることになります。

3. Native AOT の全体像(図)

Native AOT をざっくり図にすると、こうです。

通常実行dotnet publish + PublishAotC# / .NET ソースコードIL アセンブリ実行時 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 ツール
  • 短命プロセス
  • サーバーレスっぽい起動
  • コンテナの起動・入れ替え
  • 監視ツールや小さな常駐プロセス

このへんでは JIT コストが見えやすく、Native AOT でそこを前倒しできる分、初動が軽くなります。

メモリ占有も改善しやすいので、同じ台数を詰め込みたい場面でも有利です。 特に、同じプロセスが大量に立つクラウド側では、この差がじわじわ効きます。

4.2. ランタイム事前インストールを前提にしなくてよい

Native AOT で publish したアプリは、.NET ランタイム未インストール環境でも動かしやすくなります。

これは地味に大きいです。

  • 配布先に「.NET 9 Runtime を先に入れてください」と言いたくない
  • コンテナイメージを細くしたい
  • 小さいツールを 1 本だけ置いて動かしたい
  • 実行環境に JIT を許したくない / 許されない

こういう場面では、「ランタイムを別途そろえる前提」がないだけで、かなり話が静かになります。

ここでいう「ランタイム不要」は、配布先に別途 .NET を入れなくてよいという意味です。 アプリの中に必要な runtime 相当部分まで完全に消える、という話ではありません。

ランタイム不要の正しい意味Native AOTのランタイム不要とは配布先に別途.NETを入れなくてよいという意味であり、アプリの中から必要なruntime相当部分まで完全に消えるという意味ではない。ランタイム不要という言葉配布先に.NETを入れなくてよいruntime相当が消える訳ではない配布と起動の前提が減る

図8: 「ランタイム不要」は配布先の話。runtime相当はアプリの中に含まれている。

4.3. 制限のある実行環境に向く

Native AOT は、実行時 JIT を使わないので、JIT が許されない環境でも動かしやすいです。

ここは desktop よりも、クラウド、コンテナ、モバイル寄りの話で効きやすいです。 ただ、Windows 開発の文脈でも、「配布先で余計な前提を減らしたい」という意味では十分うれしい点です。

5. Native AOT で何が厳しくなるか

5.1. リフレクションと動的コード生成

Native AOT の本丸の制約はここです。

  • Assembly.LoadFile のような動的読み込み
  • System.Reflection.Emit のような実行時コード生成
  • 実行時に無制限に型をたどるリフレクション
  • 実行時に generic を好き放題組み立てるような書き方

このへんは、publish 時に必要コードを確定しにくいので、AOT warning の温床になります。

もちろん、リフレクションが 1 行でもあれば即アウト、というほど単純ではありません。 ただ、「実行時に見て決める」寄りの設計ほど厳しくなる、この傾向は外れません。

warning 名でよく出てくるのは RequiresDynamicCode 系です。 これは「その呼び出しは AOT で壊れるかもしれない」という意味なので、雑に suppress しないほうが安全です。

Native AOT をやるときは、「実行時の賢さ」を減らして、「ビルド時の明示」を増やす方向へ寄せる、と思っておくと分かりやすいです。

実行時の賢さからビルド時の明示へ動的読み込みや実行時コード生成、無制限のリフレクションはAOT warningの温床になるため、実行時の賢さを減らしてビルド時の明示を増やす方向へ設計を寄せる。実行時の賢さに頼る設計RequiresDynamicCode系のwarningビルド時の明示を増やすpublish時に必要コードが確定雑にsuppressしない

図9: 本丸の制約への向き合い方は一方向。「実行時に見て決める」を「ビルド時に明示する」へ。

5.2. trimming 前提で考える必要がある

Native AOT は trimming と深く結びついています。 ここで見落としやすいのが、自分のコードだけでなく、依存ライブラリ側の書き方も効く ことです。

要注意なのは、このあたりです。

  • reflection ベースのシリアライザー
  • 実行時スキャンで型を集める DI / プラグイン構成
  • 文字列名から型を探して実体化する仕組み
  • 動的 proxy や IL 生成に寄るライブラリ

ここで warning が出ているのに「publish は通ったからヨシ」とすると、あとでかなり渋いです。 Native AOT では、warning はだいたい本気で読んだほうがよいです。

自分のコードだけでは決まらないtrimmingtrimmingでは自分のコードだけでなく依存ライブラリ側の書き方も効き、reflectionベースのシリアライザーや実行時スキャンのDI、文字列名からの型解決、動的proxyに寄るライブラリが要注意になる。trimmingの成否自分のコードの書き方依存ライブラリの書き方reflection系や実行時スキャン系warningを本気で読んで確かめる

図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() を呼ぶなら public メソッドを残してほしい、と属性で宣言すれば消えます。

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 なら PublicMethodsGetProperty / GetProperties なら PublicPropertiesActivator.CreateInstance なら PublicParameterlessConstructorPublicConstructors です。 DynamicallyAccessedMemberTypes.All は楽ですが、対象型のメンバーをまるごと残してサイズが膨らみますし、残したメンバーが別の warning を連れてくるので、必要最小限を指定する のが原則です。

なお、属性を足しても warning が消えないときは、reflection を使っている場所から呼び出し元へさかのぼって、経路の全部に付いているか を確認します。途中で 1 か所抜けていると、そこで要求が途切れます。

属性を足してもwarningが消えないときDynamicallyAccessedMembersは必要最小限を指定し、warningが消えないときはreflectionを使っている場所から呼び出し元へさかのぼって経路の全部に属性が付いているかを確認する。1か所抜けるとそこで要求が途切れる。属性を足したのにwarningが残る呼び出し元へさかのぼる経路の全部に付いているか確認1か所抜けると要求が途切れる指定は必要最小限が原則

図11: 属性は点ではなく経路に付ける。途中で1か所抜けると要求はそこで途切れる。

5.3. プラットフォームごとに発行する

Native AOT は RID(Runtime Identifier)固定で publish します。 つまり、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 年 7 月時点)に基づきます。 WPF と WinForms については、Microsoft の「Known trimming incompatibilities」で、WPF は reflection と実行時のコード検査への依存が強く trimming 後にほぼ動かないため、WinForms は built-in COM marshalling への依存が重いため、どちらも .NET SDK 側で trimming サポートが無効化されている と明記されています。 つまり「慎重に見たほうがよい」ではなく、現状は SDK が止めている状態です。将来この記述が変わる可能性はあるので、読む時点で同じページを確認してください。

WPFとWinFormsが止められている理由WindowsのNative AOTにはbuilt-in COMが無く、WPFはreflectionと実行時コード検査への依存でtrimming後にほぼ動かず、WinFormsはbuilt-in COM marshallingへの依存が重いため、どちらも.NET SDK側でtrimmingサポートが無効化されている。WindowsのNative AOTbuilt-in COMが無いWPFreflection依存が強いWinFormsCOM marshalling依存が重いSDK側でtrimmingが無効化

図12: 「慎重に」ではなく、現状はSDKが止めている。デスクトップ本体は最初の候補にしない。

要するに、

  • WPF / WinForms 本体をいきなり Native AOT 化する
  • COM interop を普通の感覚でそのまま持ち込む

このへんは、publish 時の warning と実行時の制約が一気に増えて、対応コストが跳ね上がりやすいです。

逆に、

  • コンソール
  • worker
  • 小さな Web API
  • ネイティブ連携の中でも C の関数境界に寄せやすい部品

のほうが入り口として素直です。

COM が必要なら、JIT のままにするか、ComWrappers / source-generated COM を前提に設計し直すほうが筋がよい場面もあります。

6. 最小手順

その前に、Native AOT の publish にはネイティブのツールチェーンが要りますPublishAot を付けただけで dotnet publish を打つと、コンパイルではなく最後のネイティブリンクで落ちます。ここが最初の関門なので、先に入れておいてください。

環境 必要なもの
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 以降で対応)

要するに、コンパイラツールチェーンと、.NET ランタイムが依存するライブラリの開発用パッケージです。 CI で回すなら、dotnet/samples の Native AOT サンプルに Linux / Windows 両方の Dockerfile があるので、そこから前提のインストール手順を持ってくるのが早いです。

なお、Linux でビルドしたバイナリは 同じか、それより新しい Linux でしか動きません。Ubuntu 20.04 で作ったものは 20.04 以降では動きますが、18.04 では動きません。ビルド環境の選び方がそのまま配布可能範囲になります。

publish前の関門と配布可能範囲Native AOTのpublishにはネイティブツールチェーンが必要で、無いと最後のネイティブリンクで落ちる。またLinuxでビルドしたバイナリは同じかそれより新しいLinuxでしか動かず、ビルド環境の選び方が配布可能範囲になる。ないあるツールチェーンはあるかネイティブリンクで落ちるRIDごとのバイナリが出るLinuxは同じか新しい環境でのみ動くビルド環境の選び方が配布範囲

図13: 最初の関門はツールチェーン。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時の解析を日常的に見るのが大事。csprojにPublishAotを置く日常のdotnet runはJITのままpublish時にAOTコンパイル解析とwarningを日常的に見る

図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 固定です。 「1 本でどこでも動く .NET DLL」より、「その OS / アーキテクチャ向けに作った実行ファイル」という見方に変わります。

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 / バックグラウンドサービス
  • サーバーレスや短命プロセス
  • ネイティブアプリに差し込む小さな .NET 部品
  • 実行環境に .NET ランタイムの事前インストールを求めたくない場面

共通しているのは、境界が比較的はっきりしていて、動的な仕組みを減らしやすい ことです。

8. 向かないケース

逆に、最初から Native AOT を主戦場にしないほうがよい場面もはっきりあります。

  • WPF / WinForms の既存大規模アプリ本体
  • built-in COM interop 前提の構成
  • 実行時 plugin 読み込みが主役のアプリ
  • reflection で型探索するフレームワーク依存が強い構成
  • System.Reflection.Emit や動的 proxy を当然のように使うライブラリ
  • C++/CLI を挟んだ設計

このへんは、JIT 前提のふつうの .NET、あるいは ReadyToRun、あるいは設計の切り分け見直しのほうが筋がよいです。

9. はまりどころ

最後に、Native AOT 初手で踏みやすい点をまとめます。

  • publish warning を軽く見る
    • 前述のとおり、Native AOT の warning は本気で読む対象です。
  • build は通るのに publish で壊れる
    • publish 時に依存ライブラリまで含めた解析が本気で走るので、ここで初めて見えるものがあります。
  • ReadyToRun と Native AOT を同じ気分で扱う
    • 似た言葉ですが、制約の強さがかなり違います。
  • desktop アプリ本体からいきなり始める
    • まずは console / worker / 小さな API のほうが穏やかです。
  • JSON や設定バインドを普段のノリで書く
    • reflection 前提の書き方は、あとで効いてきます。
  • プラットフォーム非依存のつもりで配る
    • Native AOT の配布物は RID 固定です。
  • 「Native AOT = なんでも速くなる」と思う
    • 主役は起動、配布、実行環境です。ここを外すと期待値がずれます。

Native AOT では、可否を最終的に決めるのは dotnet build ではなく dotnet publish です。 ここを早めに回し始めると、後半で困りにくくなります。

可否を決めるのはpublishbuildは通るのにpublishで壊れることがあるのは、publish時に依存ライブラリまで含めた解析が本気で走るからで、Native AOTの可否を最終的に決めるのはdotnet buildではなくdotnet publishである。dotnet buildは通るまだ可否は決まっていないdotnet publish依存込みの解析が本気で走るここで初めて可否が見えるだから早めにpublishを回す

図15: はまりどころの共通項。「buildが通った」で安心せず、publishを早めに回す。

10. まとめ

Native AOT をひとことで言うと、 .NET アプリを、動的な実行モデルから、静的に確定しやすい配布モデルへ寄せる仕組み です。

見ておきたいポイントは、この 5 つに尽きます。

  1. Native AOT は publish 時にネイティブコードへ事前コンパイルする
  2. 起動、メモリ、配布の都合にはよく効く
  3. その代わり、reflection、動的コード生成、built-in COM、trimming 非対応コードには厳しい
  4. 最初の対象は、desktop 本体より console / worker / 小さな API のほうが穏やか
  5. warning を消しながら、publish ベースで早めに確かめるのが大事

Native AOT は、全部の .NET アプリに付ける標準スイッチではありません。 ですが、起動が大事、配布を軽くしたい、実行環境の前提を減らしたい という場面では、かなり強い武器です。

逆に、WPF / WinForms / COM の濃い世界では、まだ普通の .NET のほうが筋がよい場面も多いです。 ここを見分けられると、Native AOT は「難しい新機能」ではなく、使いどころのはっきりした選択肢になります。

11. 参考資料

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

Native AOTとは何ですか?
.NET アプリを publish 時にネイティブコードへ事前コンパイルして配布する発行方式です。実行時 JIT を使わないため、起動時間とメモリ占有が良くなりやすく、.NET ランタイム未インストールの環境にも配りやすくなります。その代わり、自由なリフレクション、動的コード生成、built-in COM、trimming 非対応ライブラリとは相性が悪くなります。速くなる魔法というより、起動・配布・実行環境の都合のために、動的な世界を少し捨てて静的な世界へ寄せる発行モデルです。
Native AOTとReadyToRunは何が違いますか?
ReadyToRun は IL を残したまま JIT の仕事を少し前倒しする方式で、実行時 JIT をまだ使う場面があります。互換性は広く、動的機能もだいたい使いやすいままです。一方 Native AOT は実行時 JIT 自体を前提にせず、配布物はネイティブ実行ファイル中心になり、起動はかなり改善しやすい代わりに制約が強くなります。同じ AOT という言葉が付いていても、ReadyToRun が「JIT を少し楽にする」方向、Native AOT が「JIT を前提にしない」方向で、温度感はかなり違います。
WPFやWinFormsのアプリでNative AOTは使えますか?
現時点ではかなり慎重に見たほうがよいです。Windows の Native AOT には 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 やバックグラウンドサービス、サーバーレスや短命プロセス、ネイティブアプリに差し込む小さな .NET 部品、実行環境に .NET ランタイムの事前インストールを求めたくない場面に向いています。共通するのは、境界が比較的はっきりしていて動的な仕組みを減らしやすいことです。逆に、実行時プラグイン読み込みが主役のアプリや、リフレクションで型探索するフレームワーク依存が強い構成には向きません。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る