What Is .NET Native AOT? - How It Differs from JIT and Trimming
· Go Komura · 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
- The Conclusion First (In One Line)
- The Tables to Look at First
- 2.1. The Vocabulary Around Native AOT
- 2.2. JIT vs. ReadyToRun vs. Native AOT
- The Big Picture of Native AOT (Diagram)
- 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
- 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
- Minimal Steps
- 6.1. The
csproj - 6.2. Publishing
- 6.3. How to Write JSON
- 6.1. The
- Cases Where It Fits
- Cases Where It Doesn’t
- Pitfalls
- Summary
- 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.
flowchart LR
Src["C# / .NET source code"] --> IL["IL assemblies"]
IL -->|Normal execution| JIT["Runtime JIT"]
JIT --> Run1["App runs"]
IL -->|dotnet publish + PublishAot| Analyze["AOT / trim analysis"]
Analyze --> Trim["Removal of unused code"]
Trim --> AOT["Native code generation"]
AOT --> Run2["RID-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.Emitor 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.
- Native AOT ahead-of-time compiles to native code at publish time
- It works very well for startup, memory, and distribution concerns
- In exchange, it is hard on reflection, dynamic code generation, built-in COM, and trimming-incompatible code
- As a first target, console / worker / small APIs are calmer than a desktop app body
- 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
- Native AOT deployment overview - .NET
- Native AOT deployment overview - .NET (Japanese)
- Introduction to AOT warnings - .NET
- Prepare .NET libraries for trimming - .NET
- Known trimming incompatibilities - .NET
- How to use source generation in System.Text.Json - .NET
- ASP.NET Core support for Native AOT
- ReadyToRun deployment overview - .NET
- Building native libraries - .NET
- ComWrappers source generation - .NET
- Related post: How to Turn C# into a Native DLL with Native AOT - Calling It from C/C++ via UnmanagedCallersOnly
- Related post: Why a C++/CLI Wrapper Is a Strong Choice for Using Native DLLs from C# - Compared with P/Invoke
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
What Is the .NET Generic Host? - The Foundation for DI, Configuration, and Logging
The role of the Generic Host, explained through its relationship with DI, configuration, logging, IHostedService, and BackgroundService -...
Choosing Between .NET's Three Timers - PeriodicTimer/Timer/DispatcherTimer
The differences between PeriodicTimer / System.Threading.Timer / DispatcherTimer, and how to choose between them for async processing, Th...
Calling a C# Native AOT DLL from C/C++
How to publish a C# class library as a native DLL with Native AOT and call UnmanagedCallersOnly entry points from C/C++ — when this setup...
Why Use the .NET Generic Host and BackgroundService in Desktop Apps
How to use the Generic Host and BackgroundService to organize startup, periodic processing, shutdown, logging, configuration, and DI in W...
A Practical Guide to FileSystemWatcher - Handling Missed and Duplicate Events
We organize how to use FileSystemWatcher and its pitfalls - missed events, duplicate notifications, completion-detection traps, rescans, ...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
32-bit / 64-bit Interoperability
Topic page for 32-bit / 64-bit interoperability, native boundaries, and related Windows design decisions.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
We support Windows desktop applications that involve resident processing, device integration, operational logging, and maintainable structure.
Technical Consulting & Design Review
We help clarify design direction, architectural boundaries, lifetime ownership, and how to handle legacy Windows assets.
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.
Public links