.NET MAUIでWindows向けに発行(publish)すると、exeや配布フォルダが想像以上に大きくなりがちです。多くの場合は「自己完結型(self-contained)」で.NETランタイムやWindows App SDK、NuGet依存関係まで一式を同梱しているのが主因です。ここでは、サイズが増える仕組み、縮小の現実的な手段、PublishTrimmedで起動しない理由、そして失敗しにくいdotnet publishの組み立て方を整理します。
.NET MAUI(Windows)のexeが大きくなるのは「仕様寄り」な理由がある
まず押さえたいのは、.NET MAUIのWindowsアプリで「exeだけが小さく、配布は軽い」というWin32時代の感覚は基本的に当てはまらないことです。特に次の条件が重なると、配布物全体のサイズは一気に増えます。
- 自己完結型配布(self-contained)にしている(=.NETランタイム同梱)
- Windows App SDKを自己完結(WindowsAppSDKSelfContained=true)にしている(=WinUI 3/Windows App SDKのランタイム同梱)
- サードパーティ製のNuGet(UI/DI/JSON/DB/暗号化/ログなど)を多数使っている(=DLL・ネイティブ依存・リソース増)
- 画像・フォント・ローカライズ・WebViewなど、リソース系が厚い
| 増える要因 | 中身として増えるもの | なぜ増えるか | 縮小の方向性 |
|---|---|---|---|
| Self-contained(.NET同梱) | .NETランタイム一式(多数のdll等) | 実行環境に.NETが無くても動くようにするため | 要件が許せばFramework-dependentへ |
| WindowsAppSDKSelfContained=true | Windows App SDKのフレームワークパッケージ展開物 | Windows App SDKのランタイムをアプリ側に含めるため | 要件が許せばFramework-dependentへ |
| NuGet依存が多い | 参照DLL、ネイティブ依存、リソース | 機能の対価として同梱物が増える | 依存の棚卸し、軽量ライブラリへ置換 |
| 単一ファイル化(Single-file) | 埋め込み・抽出用の仕組み | 「1ファイル」にまとめる代わりに内部へ詰め込む | 圧縮や抽出設定の調整 |
| トリミング(Trimming) | (減る)未使用コード | 静的解析で「不要」を削る | 対応できれば大きく効くが難易度高め |
つまり「大きい=失敗」ではなく、配布要件(インストール不要で動かしたい、社内PC限定、ストア配布など)に合わせてどこまで同梱するかの設計問題になりやすい、というのが現実です。
配布形態を理解すると、サイズの“増えどころ”が見える
.NET MAUI(Windows)をCLIでアンパッケージ(MSIXなし)として発行する場合、Microsoft Learnの手順では、概ね次の指定が前提になっています。
-p:WindowsPackageType=None:アンパッケージで出す(MSIXではない)-p:WindowsAppSDKSelfContained=true:Windows App SDKを自己完結で同梱する-p:RuntimeIdentifierOverride=win10-x64(またはx86):特定の既知問題回避のため
Windows App SDK側のドキュメントでも、プロジェクトは既定でframework-dependentであり、自己完結にするにはWindowsAppSDKSelfContainedをtrueにし、Windows App SDKのフレームワークパッケージが出力に展開されアプリに同梱される、と説明されています。また「.NETアプリは完全に自己完結にするなら、.NET側もself-containedとしてpublishする必要がある」と明記されています。
この2つ(.NET自己完結+Windows App SDK自己完結)が揃うと、サイズが増えるのは自然な結果です。逆に言えば、サイズを大きく減らしたいなら「同梱をやめる」以外に決定打は少ない、ということでもあります。
あなたのdotnet publishコマンドは概ね方向性は合っている(ただし“つなぎ方”が重要)
CLIでアンパッケージのMAUI Windowsアプリをpublishする際、パラメータの意味は次の整理になります。
| 指定 | 役割 | ハマりどころ |
|---|---|---|
-f netX.Y-windows10.0.19041.0 | Windows向けTFMを指定 | csprojの<TargetFrameworks>と一致させる必要がある |
-c Release | Releaseでpublish | .NET 8ではpublishの既定がReleaseのため省略可という注記あり |
-p:WindowsPackageType=None | アンパッケージ(MSIXなし) | ソリューション全体をpublishすると問題になりやすいので、アプリのcsprojにスコープする |
-p:WindowsAppSDKSelfContained=true | Windows App SDK同梱 | これをtrueにすると出力物が増える(が依存インストール不要になる) |
-p:RuntimeIdentifierOverride=win10-x64 | 既知問題回避として推奨される指定 | x64/x86をターゲットに合わせる |
-p:PublishTrimmed=true | 未使用コード削除でサイズを減らす | 反射・動的ロードが絡むと起動不能/機能欠けになりやすい(後述) |
そして、実務で多いミスが「改行した結果、オプションが同一コマンドに渡っていない」パターンです。-p:PublishTrimmed=trueを別行に書く場合、シェルごとの行継続が必要です。確実なのは、まずは1行で書いて動作確認することです。
まずは1行で確実に通す(推奨)
dotnet publish .\YourApp\YourApp.csproj -f net9.0-windows10.0.19041.0 -c Release -p:RuntimeIdentifierOverride=win10-x64 -p:WindowsPackageType=None -p:WindowsAppSDKSelfContained=true -p:PublishTrimmed=true
また、Microsoft Learnでは「ソリューションをpublishすると各プロジェクトを個別にpublishしようとして問題が起きやすいので、MAUIアプリのプロジェクトにスコープすべき」と警告しています。上の例のようにcsprojを明示するのが安全です。
Windows(cmd.exe)の行継続例
dotnet publish .\YourApp\YourApp.csproj -f net9.0-windows10.0.19041.0 -c Release ^
-p:RuntimeIdentifierOverride=win10-x64 ^
-p:WindowsPackageType=None ^
-p:WindowsAppSDKSelfContained=true ^
-p:PublishTrimmed=true
PowerShellの行継続例(バッククォート)
dotnet publish .\YourApp\YourApp.csproj -f net9.0-windows10.0.19041.0 -c Release `
-p:RuntimeIdentifierOverride=win10-x64 `
-p:WindowsPackageType=None `
-p:WindowsAppSDKSelfContained=true `
-p:PublishTrimmed=true
この“つなぎ方”の問題があると、PublishTrimmedが効いていない(あるいは別コマンドとして実行されて失敗している)のに気づきにくく、調査が長引きます。
サイズ削減は「どこまで同梱するか」を先に決めるのが近道
サイズ削減の戦略は大きく3つに分かれます。順番に検討するほど失敗しにくいです。
依存関係(NuGet)を棚卸しする
トリミングに手を出す前に、まず“不要な機能を同梱していないか”を点検します。たとえば、UI一式のライブラリ、巨大なORM、フル機能のPDF/Excel処理などは、少し使うだけでも配布物が重くなりがちです。
dotnet list packageで依存を一覧化し、使っていないものを外す- 「便利だから」で入れたユーティリティを標準APIへ置換できないか検討する
- 同系統のライブラリが複数入っていないか(JSON/DI/ログ等)確認する
この段階はトリミングより安全で、効果も読みやすいです。
Framework-dependentに寄せる(要件が許せば最も効く)
配布先が社内PCや管理端末で「前提ランタイムをインストールできる」なら、.NETランタイムやWindows App SDKを端末側に用意し、アプリはframework-dependentに寄せるのが最もサイズが下がります。Windows App SDKのドキュメントでも、既定はframework-dependentであることが示されています。
| 方針 | 配布物サイズ | 導入の手間 | 向いているケース |
|---|---|---|---|
| Self-contained(全部同梱) | 大きい | 小さい(配るだけで動く) | 個人配布・不特定多数・インストールさせたくない |
| Framework-dependent(ランタイムは端末側) | 小さい | 大きい(事前インストールが必要) | 社内配布・管理端末・インストーラーで前提を整えられる |
「サイズを絶対に小さくしたい」が最優先なら、ここが最も現実的な落としどころになります。
トリミング(PublishTrimmed)で削る(ただし難易度は高い)
トリミングは、リンク時に未使用コードを削ってサイズを下げる仕組みです。.NET MAUIではILLinkが中間コードを解析し、未使用のメンバーや型を取り除く、と説明されています。さらに、トリミング互換性は静的解析(trim warnings)で警告され、警告がある場合は修正し、十分にテストすべき、とされています。
そして重要なのが、.NETのトリミングは自己完結型(self-contained)でのpublishでのみサポートされる点です。<PublishTrimmed>true</PublishTrimmed>を指定すると、self-contained publishでトリミングされたアプリを生成し、ビルド中に互換性警告を出す、とされています。
dotnet publish -r win-x64 -p:PublishTrimmed=true
MAUI特有の観点としては、TrimMode(partial/full)で挙動が変わり、fullでは「文字列で指定するバインディングパス」など、フル・トリミングと相性が悪い機能が削除され得ることも明記されています。つまり、アプリ側の書き方や機能選択がトリミング可否に直結します。
PublishTrimmed=trueで起動しなくなる“典型原因”
結論から言うと、起動しないのは「トリマーが必要なコードまで消してしまった」可能性が高いです。トリマーは静的解析で参照関係を追いますが、次のようなパターンは解析が困難(または不可能)で、実行時に必要な型・メソッドが削られやすくなります。
| 起動不能の典型パターン | なぜ危ないか | よくある兆候 |
|---|---|---|
| 文字列で型名を指定して動的生成(例:Type.GetType) | 実行時入力に依存し、必要な型を事前に推論できない | 起動直後に例外、画面が出ない |
| Assembly.Load/LoadFromでプラグインDLLを読む | ロードされるアセンブリが何を参照するか分からず、トリム済みで欠ける | 特定機能だけ落ちる、プラグインが認識されない |
| リフレクションが濃いシリアライザー(例:XmlSerializer等) | 複雑な反射で型を走査し、トリマーが追跡できない | 設定読み込み、保存、API通信で落ちる |
| .NET MAUIの“full trimming非互換機能”を使っている | full trimmingでは機能が削除される可能性がある | バインディング/ナビゲーション/特定UI機能が動かない |
.NETのトリミング警告(例:IL2026)の説明では、RequiresUnreferencedCodeが付いたメンバーを呼び出す(または反射でアクセスする)と警告になる、とされ、代表例としてAssemblyを動的に読み込むケースやXmlSerializerのように複雑な反射を行うケースが挙げられています。これはまさに「トリムで必要なメンバーが消える」典型パターンです。
トリミングで壊れたときの現実的な直し方
「トリミングを有効にしたら起動しない」を解くには、精神論ではなく、どこがトリム非互換なのかを特定し、残す/書き換える/諦めるの三択になります。
まずは“警告を増やす”
.NET MAUIのトリミングドキュメントでは、パッケージ由来の警告が「アセンブリごとに最大1件」に抑えられることがあり、より詳細にしたい場合はTrimmerSingleWarnをfalseにすることで個別警告を有効にできる、と説明されています。警告が少なすぎて手掛かりがないときに有効です。
<PropertyGroup>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
</PropertyGroup>
“根本的に無理なパターン”は隔離する
.NETのガイドでは、実行時入力(文字列)から型を取得して生成するようなコードはトリミング互換にできない例として示され、そうした箇所はRequiresUnreferencedCodeを付けて「ここはトリミング非互換」と明確にする手順が紹介されています。これにより、呼び出し側に警告が伝播し、危険箇所が追いやすくなります。
MAUIアプリで言えば、プラグイン方式・動的XAMLロード・リフレクション前提のDI/シリアライズなどが該当しやすいです。これらを「使わない設計に寄せる」か「トリミングしない(もしくは部分的に留める)」のが、時間対効果の高い判断になります。
どうしても残したい型・メンバーは“残す指示”を出す
MAUIのトリミングドキュメントでは、トリマーが動的呼び出し(間接的なものも含む)で必要なコードを削除することがあるため、DynamicDependencyで保持するメンバーを指示できる、と説明されています。例として、動的にアセンブリを読み、型・メソッドを取得して呼ぶコードに対し、DynamicDependencyがあることで対象メソッドを保持できることが示されています。
ただし、保持指定はメンテナンスコストが上がりやすいのが難点です。依存ライブラリ更新で型名・メンバーが変わると再度壊れます。チーム開発では「保持指定を増やしすぎない」ルール作りが重要になります。
.NET MAUI側の“trim不利な機能”を避ける
MAUIドキュメントでは、full trimmingと相性が悪い機能として、文字列で指定するバインディングパス、LoadFromXamlによる実行時XAMLロード、QueryPropertyAttributeによるナビゲーションデータ受け取り等が列挙され、代替(compiled bindings、IQueryAttributableなど)に置き換えることが推奨されています。設計を寄せられるなら、ここが最も効きます。
トリミング以外でもできる「見た目のサイズ削減」
トリミングは強力ですが、難易度が高いのも事実です。そこで、壊しにくい順に“できること”をまとめます。
Single-file化と圧縮(配布体験を良くする)
.NETには単一ファイル(single-file)で配布する仕組みがあり、<PublishSingleFile>true</PublishSingleFile>でself-contained publish時に単一ファイルを生成できる、とされています。また、single-fileには互換性警告が出るため、ビルド時の分析にも役立ちます。
<PropertyGroup>
<PublishSingleFile>true</PublishSingleFile>
</PropertyGroup>
さらに、single-fileは埋め込むアセンブリを圧縮する設定があり、EnableCompressionInSingleFileをtrueにすると埋め込みアセンブリが圧縮され、exeサイズを大きく減らせる可能性がある一方、起動時に展開が走るため起動コストが増え得るので測定が推奨されています。
<PropertyGroup>
<PublishSingleFile>true</PublishSingleFile>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
</PropertyGroup>
またsingle-fileでも、既定ではコアランタイム等のネイティブDLLが別ファイルになることがあり、完全に1ファイルに寄せたい場合はIncludeNativeLibrariesForSelfExtractやIncludeAllContentForSelfExtractといったプロパティが紹介されています(抽出方式になる点に注意)。
ただし、.NET MAUI+Windows App SDK+アンパッケージという組み合わせでは、環境や依存関係によって「期待通りの単一exe」にならない・互換性問題が出ることもあります。まずは“単一ファイルは配布体験改善の手段”として捉え、サイズ・起動速度・安定性のバランスで判断するのがおすすめです。
ReadyToRunは“サイズを減らす”目的には向かない
起動を速くするためのReadyToRun(R2R)は、ILとネイティブコードの両方を含むためバイナリが大きくなり、一般にアセンブリサイズが2〜3倍に増える、と説明されています。したがって「サイズ削減」の目的で有効化するものではなく、起動速度が要件のときに検討するオプションです。
“サイズを小さくしたい”ときのチェックリスト(実務向け)
- 配布要件を確定する:インストール不要が必須か/社内配布で前提導入できるか
- publish対象はcsprojを明示:ソリューションpublishは避ける
- 依存関係を棚卸し:使っていないNuGetを削る、重い依存を置換する
- 同梱をやめられるならやめる:.NET/Windows App SDKをframework-dependentに寄せる
- トリミングは段階的に:まず警告を増やす→危険箇所を特定→残す指示 or 書き換え or 断念
- 単一ファイル+圧縮は“配布改善”として試す:サイズと起動時間を両方測る
まとめ:.NET MAUI(Windows)のサイズ問題は「削る」より「同梱を設計する」
.NET MAUIのWindowsアプリで配布物が大きくなる主因は、自己完結型配布による.NETランタイム同梱と、Windows App SDK自己完結(WindowsAppSDKSelfContained=true)によるランタイム同梱、そして多数のNuGet依存です。CLIのアンパッケージ発行は、Microsoft Learnの手順に沿ってWindowsPackageType=NoneやRuntimeIdentifierOverrideを適切に指定し、publish対象はアプリのcsprojにスコープするのが安全です。
サイズを小さくしたい場合、最も効くのは「前提導入できるならframework-dependentへ寄せる」ことです。どうしても同梱が必要なら、次に効くのがトリミングですが、反射・動的ロード・一部MAUI機能が絡むと起動不能になり得ます。警告を増やして原因を特定し、残す指定や設計変更ができない場合は、無理にトリミングを続けず安定性を優先する判断も重要です。

コメント