trimming 和 aot compilation 是两个独立阶段:trimming 在 il 层静态删减未用代码,输出仍为 il;aot 则将 il 翻译为平台原生机器码,生成无运行时依赖的二进制文件。

Trimming 和 AOT compilation 不是同一阶段的事,别混着配
很多人一看到 PublishAot=true 就顺手加 TrimMode=link,以为“一起开就更小更快”,其实它们干的是完全不同的事:Trimming 是“删代码”,AOT 是“换编译器”。不先理解分工,很容易发布失败或运行时崩掉。
- Trimming(剪裁):在 IL 层做静态分析,识别并移除**未被调用的类型、方法、程序集**——它操作的是中间语言(IL),输出仍是 IL(只是更瘦),后续仍可走 JIT;
- AOT compilation(原生 AOT):把 IL(或剪裁后的 IL)彻底翻译成目标平台的机器码(如 x64 指令),生成不含 .NET 运行时、不依赖 JIT 的独立二进制文件。
也就是说:你可以只 Trimming 不 AOT(比如发布一个瘦 DLL);也可以只 AOT 不 Trimming(但体积大、易报错);但生产环境几乎总是两者配合——先剪再编,否则 AOT 会试图编译一堆根本用不到的反射路径,直接触发 IL3050 警告甚至编译失败。
为什么单独开 AOT 很可能编译失败?看这个典型警告
如果你只设了 PublishAot=true 却没开 Trimming 或没处理反射,大概率遇到这类错误:
IL3050: Using member 'System.Type.MakeGenericType(Type[])' which has 'RequiresDynamicCodeAttribute'
这说明 AOT 编译器在扫描代码时,发现你用了 MakeGenericType 这种必须在运行时动态生成类型的 API——而 AOT 在构建期就要确定所有类型,根本没法“猜”你 runtime 会造出什么泛型实例。它不会帮你硬编,而是直接报错或警告。
- Trimming 的作用之一,就是配合 AOT 提前“锁定”哪些泛型/反射需要保留;
- 比如用
[DynamicDependency]或TrimmerRootAssembly显式声明依赖,才能让 AOT 知道:“这些类型虽然静态不可达,但请一定编进去”; - 不加 Trimming 的 AOT,默认尝试编译所有可达路径,极易撞上动态边界。
命令行和 csproj 里怎么配才真正生效?
光写 PublishAot=true 不够,.NET 9 要求你同时满足三个硬条件:目标运行时明确、自包含、输出类型为 Exe。缺一不可。
- CLI 命令必须带
-r(runtime identifier)和--self-contained true:dotnet publish -r win-x64 --self-contained true -p:PublishAot=true -p:TrimMode=link - csproj 中至少要有:
<outputtype>Exe</outputtype><targetframework>net9.0</targetframework><publishaot>true</publishaot><trimmode>link</trimmode> - 注意:
TrimMode=link是推荐值,TrimMode=copyused只复制引用但不删 IL,对 AOT 几乎无效。
实际效果差异:体积和启动时间不是线性叠加的
有人测过:只 Trimming 可减 30% 体积,只 AOT 启动快 2×,但两者合体后,启动时间不是“2× + 30%”,而是常达 4× 以上提升——因为 AOT 避开了 JIT 预热,而 Trimming 让 AOT 编译器要处理的代码量锐减,连带减少了元数据体积和符号表大小。
- 典型 Web API 冷启动:
JIT 模式约 850ms → AOT+Trimming 后压到 210ms; - 但若忘了关调试元数据,加个
-p:IlcGenerateStackTraceData=false,还能再省 10–15MB; - 反过来,如果用了
Activator.CreateInstance却没加[UnconditionalSuppressMessage]或根注册,哪怕体积再小,运行时也会抛MissingMethodException。
最常被忽略的一点:AOT 不是“设了就赢”,它把很多运行时问题提前暴露到了构建期——那些曾经 JIT 下侥幸跑通的反射黑魔法,现在会直接卡在 dotnet publish 这一步。早测,比晚救强得多。










