What Is .NET Native AOT? - How It Differs from JIT and Trimming

· · C#, .NET, Native AOT, Publishing, Design

In How to Turn C# into a Native DLL with Native AOT - Calling It from C/C++ via UnmanagedCallersOnly, we already covered using Native AOT to call C# from C/C++. Honestly, though, it would have been kinder to put what Native AOT actually is first. The order got slightly reversed.

Discussions of Native AOT are notorious for terms blurring together right at the start.

  • Is it about getting rid of the JIT?
  • How does it differ from self-contained and single-file?
  • Is it in the same family as ReadyToRun?
  • What’s actually happening when a flood of trimming warnings appears?
  • Can WPF / WinForms / ASP.NET Core all use it with the same level of comfort?

When these blur together, Native AOT starts to look like either “magic that just makes things faster” or, conversely, “a scary thing full of restrictions.” Both views are a bit sloppy.

In this article, assuming the current practical landscape of .NET 8 and later, we sort out these four things first.

  • What Native AOT really is
  • What you gain, and where things get tough
  • How it differs from ReadyToRun and trimming
  • Which kinds of apps make for a gentle first attempt

Table of Contents

  1. The Conclusion First (In One Line)
  2. The Tables to Look at First
    • 2.1. The Vocabulary Around Native AOT
    • 2.2. JIT vs. ReadyToRun vs. Native AOT
  3. The Big Picture of Native AOT (Diagram)
  4. What Native AOT Gives You
    • 4.1. Startup Tends to Get Lighter
    • 4.2. No Need to Assume a Pre-Installed Runtime
    • 4.3. Good Fit for Restricted Execution Environments
  5. Where Native AOT Gets Tough
    • 5.1. Reflection and Dynamic Code Generation
    • 5.2. You Have to Think Trimming-First
    • 5.3. Publishing per Platform
    • 5.4. Windows Desktop / COM Contexts Call for Real Caution
  6. Minimal Steps
    • 6.1. The csproj
    • 6.2. Publishing
    • 6.3. How to Write JSON
  7. Cases Where It Fits
  8. Cases Where It Doesn’t
  9. Pitfalls
  10. Summary
  11. References

1. The Conclusion First (In One Line)

  • Native AOT is a publishing model that ahead-of-time compiles a .NET app to native code at publish time and ships that.
  • Because it doesn’t use a runtime JIT, startup time and memory footprint tend to improve, and it is easy to deploy to machines without the .NET runtime installed.
  • In exchange, it gets along poorly with unrestricted reflection, dynamic code generation, built-in COM, and libraries that don’t support trimming.
  • In other words, rather than speed magic, it is a publishing model that trades away a bit of the dynamic world and leans into the static one, for the sake of startup, distribution, and execution-environment constraints.

Native AOT is “a mechanism for shipping .NET like a native app” — not a simple checkbox for faster compilation.

2. The Tables to Look at First

2.1. The Vocabulary Around Native AOT

Separating these terms up front makes everything that follows easier.

Term What it does Relationship to Native AOT
JIT Generates native code from IL at runtime Native AOT does this work ahead of time
self-contained Ships the required .NET bits along with the app Native AOT belongs to this family of thinking
single-file Bundles the distribution into one file Distinct from the essence of Native AOT, but the end result often looks similar
trimming Removes unused code Practically a prerequisite under Native AOT
ReadyToRun Keeps the IL but front-loads some of the JIT’s work Similar-sounding but a genuinely different thing
source generator Moves runtime dynamic behavior into build-time code generation Pairs well with Native AOT

The confusing part is that Native AOT is less a single feature and more a publishing model that works hand in hand with self-contained, trimming, source generation, and RID-pinned publishing.

2.2. JIT vs. ReadyToRun vs. Native AOT

This too is fastest to absorb as one table.

Aspect Ordinary JIT execution ReadyToRun Native AOT
Runtime JIT Used Still used in some cases Not used
Distribution contents Mostly IL IL + pre-generated code Mostly a native executable
Startup Baseline Easy to improve Very easy to improve
Compatibility Broadest Broad Strongly constrained
Dynamic features Easy to use Mostly easy to use Heavily restricted
Best suited for General .NET development A first step toward startup improvement Aggressively pursuing startup, distribution, and restricted environments

If ReadyToRun is the direction of “making the JIT’s life a little easier,” Native AOT is the direction of “not assuming a runtime JIT at all.” The same letters A-O-T appear, but the temperature is quite different.

3. The Big Picture of Native AOT (Diagram)

Roughly sketched, Native AOT looks like this.

Normal executiondotnet publish + PublishAotC# / .NET source codeIL assembliesRuntime JITApp runsAOT / trim analysisRemoval of unused codeNative code generationRID-specific executable

Ordinary .NET first produces IL, then JIT-compiles only what is needed at runtime. Native AOT front-loads a large portion of that later stage to publish time.

The crucial point is that at publish time, the toolchain “needs to know essentially all the code that will be needed at runtime.” This is where the atmosphere changes.

  • Discovering types at runtime
  • Growing code at runtime
  • Loading assemblies at runtime
  • Deferring resolution with a runtime “eh, it’ll work out”

Code written in these styles suddenly stops getting along with Native AOT.

4. What Native AOT Gives You

4.1. Startup Tends to Get Lighter

The most visible payoff of Native AOT is, unsurprisingly, startup.

  • CLI tools
  • Short-lived processes
  • Serverless-style startup
  • Container startup and rollover
  • Monitoring tools and small resident processes

In these scenarios the JIT cost is easy to see, and because Native AOT front-loads it, the first moments get lighter.

Memory footprint also tends to improve, which helps when you want to pack more instances onto the same hardware. Especially on the cloud side, where many copies of the same process spin up, this difference compounds steadily.

4.2. No Need to Assume a Pre-Installed Runtime

An app published with Native AOT is easy to run on machines without the .NET runtime installed.

This is quietly significant.

  • You don’t want to tell deployment targets “please install the .NET 9 Runtime first”
  • You want slimmer container images
  • You want to drop a single small tool somewhere and have it run
  • You don’t want — or aren’t allowed — to permit JIT in the execution environment

In such situations, simply removing the “runtime must be provisioned separately” prerequisite makes everything much calmer.

Note that “no runtime needed” here means the deployment target doesn’t need a separate .NET install. It does not mean the runtime-equivalent parts inside the app vanish entirely.

4.3. Good Fit for Restricted Execution Environments

Because Native AOT doesn’t use a runtime JIT, it is easier to run in environments where JIT is not permitted.

This benefit lands more on the cloud, container, and mobile side than on desktop. Still, even in a Windows development context, it remains a genuine win in the sense of “reducing extra prerequisites at the deployment target.”

5. Where Native AOT Gets Tough

5.1. Reflection and Dynamic Code Generation

This is the heart of Native AOT’s constraints.

  • Dynamic loading like Assembly.LoadFile
  • Runtime code generation like System.Reflection.Emit
  • Reflection that walks types without restriction at runtime
  • Code that freely composes generics at runtime

These make it hard to pin down the required code at publish time, so they become breeding grounds for AOT warnings.

Of course, it isn’t as simple as “one line of reflection and you’re out.” But the trend holds without exception: the more your design leans toward “look at runtime and decide,” the tougher things get.

The warning names you’ll see most often are the RequiresDynamicCode family. They mean “this call may break under AOT,” so it is safer not to suppress them casually.

When doing Native AOT, a useful mental model is: reduce “runtime cleverness” and increase “build-time explicitness.”

5.2. You Have to Think Trimming-First

Native AOT is deeply intertwined with trimming. The thing that’s easy to miss here is that not just your own code but the way your dependency libraries are written matters too.

The usual suspects are:

  • Reflection-based serializers
  • DI / plugin setups that gather types via runtime scanning
  • Mechanisms that look up types by string name and instantiate them
  • Libraries that lean on dynamic proxies or IL generation

If warnings appear here and you shrug with “publish succeeded, so we’re good,” the bill arrives later and it is steep. Under Native AOT, warnings genuinely deserve a serious read.

5.3. Publishing per Platform

Native AOT publishes pinned to a RID (Runtime Identifier). That is, this is not a world where something built for win-x64 runs as is on linux-x64.

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

You produce a publish output per target, like that.

This feels much more “native-app-like” than ordinary framework-dependent .NET.

5.4. Windows Desktop / COM Contexts Call for Real Caution

In KomuraSoft’s line of work, this part matters especially.

On Windows, Native AOT has no built-in COM. On top of that, WPF gets along poorly with trimming, and WinForms depends heavily on built-in COM marshalling — so at least for now, neither should be anywhere near the top of your “first Native AOT candidate” list.

In short, these tend to make the room go quiet:

  • Converting an existing WPF / WinForms app body straight to Native AOT
  • Bringing COM interop along with your usual instincts intact

Conversely, these are far more natural entry points:

  • Console apps
  • Workers
  • Small web APIs
  • Native-interop components that can be flattened onto a C function boundary

If you need COM, there are cases where staying on the JIT, or redesigning around ComWrappers / source-generated COM, is the sounder route.

6. Minimal Steps

6.1. The csproj

First, add PublishAot to the project file.

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

net8.0 is fine for the samples. The thinking is essentially the same on .NET 9 / 10.

What matters is not to tack it temporarily onto the dotnet publish command line, but to keep it in the project all the time, so you see the build / publish analysis on a daily basis.

Note that adding <PublishAot>true</PublishAot> does not suddenly make your everyday local runs Native AOT. Day-to-day dotnet run and normal execution stay on the JIT; the real Native AOT compilation happens at publish time.

6.2. Publishing

For Windows x64, for example:

dotnet publish -c Release -r win-x64

For Linux x64:

dotnet publish -c Release -r linux-x64

The output is RID-pinned. The mindset shifts from “one .NET DLL that runs anywhere” to “an executable built for that OS / architecture.”

If you’re approaching it from the web API side, starting from the Native AOT template is the easy way in.

dotnet new webapiaot -o MyFirstAotWebApi

For a worker, this one:

dotnet new worker -o WorkerWithAot --aot

6.3. How to Write JSON

A quietly frequent collision point under Native AOT is JSON. Used with your usual instincts, System.Text.Json leans toward reflection, so it is calmer to lean into 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);

In practice, the rule of thumb that rarely misses is less “make it Native AOT compatible” and more “don’t make the runtime go hunting for types.”

7. Cases Where It Fits

Native AOT tends to click pleasantly in cases like these.

  • CLI tools where startup is the star
  • Small APIs deployed at scale in containers
  • Workers / background services
  • Serverless and short-lived processes
  • Small .NET components slotted into native apps
  • Situations where you don’t want to require a pre-installed .NET runtime

What they share is that the boundaries are relatively clear and dynamic machinery is easy to reduce.

8. Cases Where It Doesn’t

Conversely, there are clear cases where Native AOT should not be your main battlefield from the start.

  • The body of an existing large WPF / WinForms application
  • Architectures premised on built-in COM interop
  • Apps where runtime plugin loading is the centerpiece
  • Heavy dependence on frameworks that discover types via reflection
  • Libraries that use System.Reflection.Emit or dynamic proxies as a matter of course
  • Designs with C++/CLI in the middle

For these, ordinary JIT-based .NET, or ReadyToRun, or rethinking the design boundaries is the sounder route.

9. Pitfalls

Finally, the things that are easy to step on in your first round with Native AOT.

  • Taking publish warnings lightly
    • As noted above, Native AOT warnings deserve a serious read.
  • Build passes but publish breaks
    • At publish time, the analysis genuinely runs across your dependencies too, and some things only become visible there.
  • Treating ReadyToRun and Native AOT with the same mindset
    • The words are similar; the strength of the constraints is quite different.
  • Starting straight from the body of a desktop app
    • Console / worker / small API first is much calmer.
  • Writing JSON or configuration binding with your usual habits
    • Reflection-premised code comes back to bite later.
  • Distributing as if it were platform-independent
    • Native AOT outputs are RID-pinned.
  • Believing “Native AOT = everything gets faster”
    • The stars are startup, distribution, and the execution environment. Miss that, and expectations drift.

Under Native AOT, dotnet publish acts more like the referee than dotnet build does. Start running it early, and the late game gets much less painful.

10. Summary

In one sentence, Native AOT is a mechanism that shifts a .NET app from a dynamic execution model toward a distribution model that can be pinned down statically.

The points worth keeping in view come down to these five.

  1. Native AOT ahead-of-time compiles to native code at publish time
  2. It works very well for startup, memory, and distribution concerns
  3. In exchange, it is hard on reflection, dynamic code generation, built-in COM, and trimming-incompatible code
  4. As a first target, console / worker / small APIs are calmer than a desktop app body
  5. Verify early and often via publish, clearing warnings as you go

Native AOT is not a standard switch to flip on every .NET app. But where startup matters, you want lighter distribution, and you want fewer execution-environment prerequisites, it is a genuinely powerful weapon.

Conversely, in the dense world of WPF / WinForms / COM, ordinary .NET is still often the sounder choice. Once you can tell these apart, Native AOT stops being “a difficult new feature” and becomes an option with a clearly defined place.

11. References

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog