100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
.NET

Trimming and AOT Compilation

How IL trimming and Native AOT shrink and speed up .NET deployments by removing unused code and compiling ahead of time, and the reflection constraints that come with it.

PerformanceAdvanced11 min readJul 10, 2026
Analogies

IL Trimming: Shipping Only What You Use

By default, a self-contained .NET Core deployment bundles the entire shared framework so it can run without a separately installed runtime, but most applications only ever touch a small fraction of that framework's types and methods. IL trimming, enabled with <PublishTrimmed>true</PublishTrimmed> in a self-contained publish, performs static analysis starting from your application's entry point and follows every reachable code path (method calls, virtual dispatch targets, reflection-visible members you've explicitly annotated) to build a call graph, then strips any assembly, type, or member the analysis proves is unreachable. The payoff is a dramatically smaller deployment, which matters for container images, serverless cold-start size, and edge/IoT devices, but the risk is that trimming is unaware of code reached only through unannotated reflection, so a type loaded dynamically via Activator.CreateInstance(string) with no matching DynamicallyAccessedMembers attribute can be trimmed away and throw at runtime instead of compile time.

🏏

Cricket analogy: It's like a touring squad's management packing only the kit that specific tour's fixtures actually require, based on the confirmed match schedule, rather than the entire national federation's full equipment inventory, leaving lighter but risking a gap if an unscheduled extra fixture gets added.

Native AOT: Compiling Ahead of Time

Native AOT, stabilized in .NET 8, goes further than trimming by compiling your application directly to native machine code at publish time rather than shipping IL that the JIT compiles at startup. Because there's no JIT warming up tiers 0 through 1 at runtime, Native AOT apps start dramatically faster (often single-digit milliseconds for simple console or minimal API apps) and have a smaller memory footprint, which matters enormously for serverless functions that pay a cold-start penalty and for CLI tools that run thousands of times in CI pipelines. The tradeoff is that Native AOT imposes hard constraints that the regular JIT-based runtime doesn't: no runtime code generation (so System.Reflection.Emit and dynamic assembly loading are unavailable), reflection is limited to what the trimmer/AOT analysis can statically prove is needed, and some libraries relying on unbounded reflection (like certain older ORMs or serializers) simply won't work without an AOT-compatible rewrite.

🏏

Cricket analogy: It's like a bowler who has fully mastered a yorker in the nets so it's automatic on delivery day, versus one still visibly adjusting grip mid-run-up during the actual over; Native AOT is the pre-mastered delivery with no in-game warm-up needed.

Trim and AOT Compatibility Analysis

The .NET SDK ships Roslyn analyzers (enabled via <EnableTrimAnalyzer> and <IsAotCompatible>) that flag trim- and AOT-unsafe patterns directly in your editor, such as calling Type.GetType(string) with a non-constant string, using MakeGenericMethod with types not statically provable, or reflecting over members without a DynamicallyAccessedMembers attribute describing what the trimmer must preserve. Library authors targeting AOT-compatible consumers annotate their public APIs with attributes like RequiresUnreferencedCode and DynamicallyAccessedMembersAttribute so the trimmer knows exactly which members must be kept alive even though no static call site references them directly, which is exactly how System.Text.Json's source generator mode (JsonSerializerContext) avoids needing runtime reflection entirely, making it fully AOT-safe unlike its older reflection-based serialization path.

🏏

Cricket analogy: It's like a third umpire's checklist that flags exactly which replay angles are legally admissible before a decision is given, rather than discovering mid-review that a required camera angle was never recorded; the analyzer catches gaps before they become runtime failures.

csharp
// .csproj settings for trimming and Native AOT
// <PropertyGroup>
//   <PublishTrimmed>true</PublishTrimmed>
//   <PublishAot>true</PublishAot>
//   <TrimMode>full</TrimMode>
//   <EnableTrimAnalyzer>true</EnableTrimAnalyzer>
// </PropertyGroup>

// AOT-safe JSON serialization via source generation (no runtime reflection)
[JsonSerializable(typeof(WeatherForecast))]
internal partial class AppJsonContext : JsonSerializerContext { }

public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary);

public class Program
{
    public static void Main()
    {
        var forecast = new WeatherForecast(DateOnly.FromDateTime(DateTime.Now), 22, "Mild");

        // Uses the source-generated context instead of reflection-based
        // serialization, which is required for full Native AOT compatibility.
        string json = JsonSerializer.Serialize(forecast, AppJsonContext.Default.WeatherForecast);
        Console.WriteLine(json);
    }
}

Native AOT disallows System.Reflection.Emit and unconstrained runtime code generation entirely, not just at reduced performance. If a library you depend on dynamically emits IL (common in some older ORMs, mocking frameworks, or dependency injection containers), it will fail to compile or throw at publish/runtime under PublishAot, and there is no workaround short of replacing that dependency with an AOT-compatible alternative.

Run dotnet publish -p:PublishAot=true locally and check the build warnings before assuming a library is AOT-compatible; the trim/AOT analyzers surface incompatibilities as build-time warnings (like IL2026 or IL3050) well before you'd otherwise discover them as runtime exceptions in production.

  • IL trimming removes unreachable assemblies, types, and members from a self-contained publish based on static reachability analysis.
  • Trimming reduces deployment size significantly, which matters for containers, serverless cold starts, and edge devices.
  • Unannotated reflection (like Activator.CreateInstance(string) with a dynamic type name) can be trimmed away and fail at runtime.
  • Native AOT compiles directly to native code at publish time, eliminating JIT warm-up for near-instant startup and lower memory use.
  • Native AOT disallows runtime code generation (Reflection.Emit) and constrains reflection to what static analysis can prove.
  • Trim and AOT analyzers (EnableTrimAnalyzer, IsAotCompatible) surface incompatibilities like IL2026/IL3050 as build warnings.
  • System.Text.Json's source-generated JsonSerializerContext avoids runtime reflection entirely, making it fully AOT-safe.

Practice what you learned

Was this page helpful?

Topics covered

#NET#NETCoreStudyNotes#MicrosoftTechnologies#TrimmingAndAOTCompilation#Trimming#AOT#Compilation#Shipping#StudyNotes#SkillVeris